一、脚本
1.1 玩转npm scripts
#常用命令
- npm init :初始化工程
- npm run :run script
- npm install :安装依赖
- npm update :升级依赖
- npm bin:查看bin文件目录
- npm link :将工程软连接到全局
- npm publish :发布包
- npm deprecate :废弃包
1.1.1 内部变量
问题:输出the package is xxx@x.x.x(输出package里的一些信息)
答案:$npm_package_*
$npm_package_name # 输出package里的name
$npm_package_version # 输出package里的name
$npm_package_config_var1 # 输出package里的config里的var1
当我们npm run 执行脚本的时候,npm读取package转换为shell变量,所以我们才能在脚本中拿到这些变量
#1.1.2 npm scripts -参数
问题:如何对npm scripts二次包装多的命令传参
答案:利用--透传参数
{
"script":{
"serve":"server ./build",
"serve":"npm run serve -- -l 80"
}
}
1.1.3 npm scripts -脚本钩子
- 脚本钩子类似于hook,当事件触发时,对应的钩子逻辑也被触发,git hook、web hook等
- 部分npm内置脚本钩子如下:
preinstall # 用户执行npm install命令时候,先执行脚本
postinstall # 用户执行npm install命令是,安装结束后执行该脚本
preuninstall # 卸载一个模块前执行
postuninstall # 卸载一个模块后执行
prelink # link模块前执行
postlink # link模块后执行
pretest # 运行npm test命令前执行
posttest # 运行npm test命令后执行
- 规律:pre-* 和 post-*
除了内置脚本钩子,我们也可以按照规则自定义添加钩子
- 例子:自动化发版
#!/usr/bin/env node
const semver=require('semver')
const packageInfo=require('../pageage.json')
const fs=require('fs')
const targetVersion=semver.inc(packageInfo.version)
packageInfo.version=targetVersion
...
#1.1.4 思考
- 为什么脚本第一行需要有
#!usr/bin/env node
#!是一个特殊的标示符,后面跟的是解释脚本的路径,说明这个文件可以当作脚本来运行,usr/bin/env/ node 表示通过env来运行node,env将会从环境变量中查找node工具
- 如果想在一条script里顺序执行两个命令,应该怎么写?
用;隔开
- 如果想在一条script里并行执行两个命令?
用&&隔开
1.2 bash简洁和快速入门
#shell是什么?
- shell 不仅仅是命令行,也可以是GUI
- Shell 是操作系统和用户交互的“接口”
- 一般来说,我们说的Shell都是Unix Shell,可以任务是CLI
#命令(Command)是什么?
- 命令的本质是一个程序
- 这些程序具有发起系统调用(System call)的能力
- 编写shell脚本,其实是在编排这些程序的执行
- 除此之外,还有shell语法解释器负责解释一行行的Shell语句
#Shell解释器
- bash (linux/Unix)
- sh (linux/Unix)
- zsh (linux/Unix)
- cmd (windows)
- PowerShell (windows)
- WSL (Windows Subsysstem of linux)
#1.2.1 常用的bash命令
#其实我们平时已经使用了很多常用bash命令
cd ./xxx
ls -al
rm -rf ./*.log
mkdir react-demo
ps -a u x # ps 查看进程
kill 3790 # 杀掉进程
没有什么是-h解决不了的,如果有就用man
有时候命令太多,参数太多,我们不能全部记得,我们可以man去查看一个命令的用法
#例如
man ps
# 退出,按字母q
q
文件新建
- touch(新建文件)
touch ./index.js
- mkdir(新建文件夹)
mkdir ./project
#文件删除
- rmdir
rmdir ./project
- rm
rm ./projcet
rm -r ./project # 递归删除
rm -rf ./project # 强制递归删除
#文件删移动
- mv
mv ./source.txt ./target
mv -f ./source/a.txt ./target # 移动并强制覆盖
mv -n ./source/a.txt ./target # 移动不覆盖
- cp
cp ./source.txt ./target/
cp -R ./source/a.txt ./target/ # 递归复制
#文件查看
cat、head、tail
cat package.json # 查看文件
head -n 10 ~/.logs/sevive-a.log # 查看文件的前10行
tail -n 10 ~/.logs/sevive-a.log # 查看文件的后10行
文件编辑
- nano
GNU nano是linux上最简单的文本编辑器,操作简单,功能也比较初级,对于一些临时和简单的文件编辑操作,我们可以直接使用nano就好
- vi/vim
vi是linux上的一款功能强大的编辑器,vim更是vi的加强版。vim和emacs都是cli世界中的编辑器王者,如果能够熟练使用,效率完全不输于现代的GUI编辑器(如vscode),但是由于使用比较复杂,内容超出了本节的范围
#进程相关
- ps
ps # 查看当前用户进程
ps -ax # 查看所有进程
- lsof
lsof -i # 查看打开网络的相关文件
lsof -p 2333 # 查看pid=233的进程打开的文件
- top
top # 查看实时的电脑使用情况
- kill
kill 45934 # SIGTERM信号
kill -9 45934 # SIGKILL信号,强杀进程
kill命令实际上并不是在“kill”,本质是向进程发送信号。例如:kill-s SIGUSR1 34534 实际上可以调试 Nodejs应用,因为Nodejs会在收到SIGUSR1时进入调试模式
优雅退出的原理就是监听SIGTERM信号,并递归退出子进程
#其他
- grep (对结果的每一行进行筛选)
lsof -i | grep LISTEN # 找到所有正在被监听的端口
- awk
docker rm $(docker ps -a | awk 'NR>1{print $1}') # 删除所有的docker容器
chmod +x ${ls -al | awk 'NR>1{print $9}'} # 为当前目录下的所有文件添加可执行权限
以上命令用法并不重要,知道怎么找到命令的方法才重要,特别记忆的知识,都是你暂时用不到的,我们应该把精力放在学习思想和方法上
1.2.2 bash编程-变量
- 全局变量
# 无需关键字,等号2变不要有空格
COURES=ENGINEERING
export COURES=ENGINEERING # 导出
- 局部变量
# 用户函数内,作用域的概念
local COURES=ENGINEERING
- 环境变量
PATH:指定目录的搜索路径
HOME:指定用户的主目录(即登录用户到linux系统中时默认的目录
HISTSIZE:指保存历史命令记录的条数
LOGNAME:指当前用户的登录名
HOSTNAME:指主机的名称,许多应用程序如果要用到主机名的话,通常是从这个环境变量中来取得
SHELL:指当前用户用的是那种shell
LANG/LANGUGE:和语言相关的环境变量,使用多种语言的用户可以修改此环境变量
MAIL:指当前用户的邮件存放系统
# 我们可以在控制台输出,例如
# echo 相当于js的console.log
echo $SHELL
# 结果:/bin/bash
我们常说把某个命令加入环境变量中,其实就是加入$PATH环境变量
- 基本类型
# string
ASRING=abc
ASRING="acs"
# number
ANUMBER=$[1+1]
ANUMBER=$((1+1))
# Array
ANARRAY=(what's the day today)
ANARRAY=(1 2 3 4)
ANARRAY[1]=0
1.2.3 bash编程-运算
- 组合
ASTRING=abd
ANUMBER=$((1+1))
STR ="The starts $ASTRING"
eacho $STR # The starts abd
SEQ=(1 $ANUMBER 3 4 5)
echo $SEQ # 1 2 3 4 5
- 数学运算符
ANUMBER=$(6+2)
ANUMBER=$(6-2)
ANUMBER=$(6*2)
ANUMBER=$(6/2)
#1.2.4 bash编程-条件语句
- if then
if conditicon1
then
command1
elif conditicon1
then command2
else commandN
fi # 结束关键字
- case
case $VAR in
condition1)
command1
;;
condition2)
echo command2
;;
*)
acho command3
;;
asac
- 比较符
-z var # 检查变量var是否为空
-d file # 检查file是否存在并是一个目录
-e file # 检查file是否存在
-f file # 检查file是否存在并是一个文件
-r file # 检查file是否存在并可读
-s file # 检查file是否存在并非空
-W file # 检查file是否存在并可写
-X file # 检查file是否存在并可执行
-O file # 检查file是否存在并属于当前用户
-G file # 检查file是否存在并其默认租与当前用户相同
file1 -nt file2 # 检查file1是否比file2新
file1 -ot file2 # 检查file1是否比file2旧
#1.2.5 bash编程-循环语句
- for循环
for index in 1 2 3 4 5; do
echo "index="$index
done
for((i=0;i<5;i++));do
echo $i
- while循环
while (($i<=10))do
echo $i
done
1.2.6 bash编程-循环语句
- 函数的定义
function custom()
{
# 定义一个变量
local prefix="input is"
# $1是否为空
if [-z $1]; then
echo “no input”
else
echo “$prefix $1”
fi
return 0
}
# 在函数体中,可以使用$n来获取第n个实参
- 函数的调用和返回值
custom # unknown 调用
custom abc # input is abc 调用并传入abc
echo $? # 0 $?拿到上一次函数调用的结果
shell中运行的每一个命令都使用退出状态码(exit status)来告诉shell它完成了处理,退出状态码是一个0-255 之间的整数值,在命令结束运行时由命令传给shell,可以在命令执行完毕后立即使用$?捕获。
- 其他特殊变量
$# 传递到脚本或函数的参数个数
$* 以一个单字符串显示所有向脚本传递的参数
$$ 脚本运行的当前进程ID号
$! 后台运行的最后一个进程的ID号
$@ 与$*相同,但是使用时加引号,并在引号中返回每个参数
$- 显示shell使用的当前选项,与set命令功能相同
$? 显示最后命令的退出状态,0表示没有错误,其他任何值表明有错误
1.2.7 bash编程-重定向
-
什么是重定向
- 重定向,全称I/O重定向,默认情况下,Bash程序从终端接受输入,并在终端打印输出(标准输入,标准输出)
- 如果你想改变输入的来源,或是输出的目的地,那么就需要使用“重定向”
-
怎么用?只要记住四个符号
# 将command命令执行的结果重定向到file中
command > file # 将输出重定向到file
# 将file个文件的内容作为command的输入内容
command < file # 将输入重定向到file
# 将file个文件的部分内容作为command的输入
command << file # 将输入重定向到file的部分内容
# 一般输出log文件
command >> file # 将输出以追加的方式重定向到file
案例:把ls输出到终端的信息输出到一个文件中
ls # 可以查看当前文件的文件信息
ls > ls.log # 在当前文件夹下生成了一个ls.log文件,文件里的内容是ls输出的信息
ls -al > ls.log # 把ls -al 输出的详细信息输入到ls.log文件中,这种做法会覆盖上一次文件里的内容,我们可以使用>>追加的方式
ls >> ls.log # 会把ls输出的信息放到ls -al 输出信息的后面
#1.2.8 bash编程-交互式程序
- echo和read
echo “xxx” # 打印并换行
echo -n “xxx” # 打印且不换行
read var # 读取输入,存变量var
read -n 1 var # 读取输入的一个字符,存入变量var
案例:可以在终端输入以下命令进行体验
echo -n "what ur first name?";\
read firstname;\
echo -n "What's ur second name?";\
read secondname;\
echo "$firstname $secondname";
# 询问你firstname 和 secondname,当你输入后,最后会打印你输出的内容
#1.2.9 扩展学习资料
这是一本全面而详细的介绍Linux操作系统的好书,适合对Linux操作系统有兴趣和需要熟悉Linux环境的同学。
这是一本深入浅出的介绍Linux命令和Shell脚本编写的优秀技术书,目前豆瓣评分9.3分。如果你想深入和熟练的掌握Shell编程,希望你不要错过它。
1.3 浅谈Node CLI
#从process.argv说起
process是node的进程模块,process有个argv属性来获取node进程获取命令行参数
代码
process.argv.forEach((val,index)=>{
console.log(`${index}:${val}`)
})
执行
node process-argv.js one two three
结果
0:/usr/local/bin/node
1:/Users/hejialiang/Desktop/work/个人代码/vue/Senior-FrontEnd/examples/engineering/2.3/process-argv.js
2:one
3:two
process.argv 属性返回一个数组,其中包含当启动Node.js进程时传入的命令行参数。
第一个元素是process.execPath,第二个元素将是正在执行javascript文件的路径,其余元素将是任何其他命令行参数。
1.3.1 commander(更方便的cli参数处理,作者tj)
- 链式调用
- 更好的参数处理
- autohelp
#!/usr/bin/env node
const program=require('commander')
program
.name('better-clone')// cli 的名字
.version('0.0.1') // 版本
.option('-v,--verbose','verposity that can be increased') // -v 简写 --verbose全称 后面是描述
// 给program添加子命令,可以用command这个方法
program
.command('clone <source> [destination]') // clone 是子命令,source是必填参数,destination 是选填参数
.option('-d,--depths <level>','git clone depths')
.description('cloe a repository into a newly created directory')
.action((source,destination,cmdObj)=>{ // cmdObj存放所有option的键值对
console.log(`start cloning from ${source} to ${destination} with depth ${cmdObj.depths}`);
})
program.parse(process.argv) // 从process.argv中取得命令行参数
// 命令行运行:node ./commander.js clone ./src ./to --depths=2
// 输出:start cloning from ./src to ./to with depth 2
#1.3.2 cli交互-inquirer.js(更友好的输入)
- 灵活的CLI交互方式
input、number、confirm、list、rawlist、expand、checkbox、password、Editor......
- 磨平平台差异
兼容Windows/OSX/Linux上的主流终端,不用关心平台底层的实现细节
const inquirer = require('inquirer')
inquirer
.prompt([
/* Pass your questions in here */
{ type: 'input', name: 'username', message: "What's ur name?" },
{
type: 'checkbox',
name: 'gender',
message: "What's ur gender?",
choices: [ 'male', 'female' ]
},
{
type: 'number',
name: 'age',
message: 'How old are u?',
validate: input => Number.isNaN(Number(input))
? 'Number Required!' : true
},
{
type: 'password',
name: 'secret',
message: 'Tell me a secret.',
mask: 'x'
}
])
.then(answers => {
console.log(`Answers are:\n ${JSON.stringify(answers)}`)
})
.catch(error => {
if (error.isTtyError) {
// Prompt couldn't be rendered in the current environment
} else {
// Something else when wrong
}
})
// 运行 node inquirer.js
// 提供输入、选择
// 能拿到命令中用户输入的和选择的参数
#1.3.3 cli交互(chalk)-更友好的输出
- 非常简单的用法
const chalk=require('chalk')
const log=console.log
const chalk=require('chalk')
const log=console.log
log(chalk.blue('\nhello')+'world'+chalk.red('!\n'))
log(chalk.blue.bgRed.bold('Hello world!\n'))
log(chalk.blue('Hello','word','Foo','bar','biz','baz\n'))
log(chalk.red('Hello',chalk.underline.bgBlue('word')+'!\n'))
chalk为什么能输出颜色?ANSI Escape Code
#1.3.4 调用其他程序(shell.js 、execa)
- CLI程序的复用
不用再重复发明git/npm/yarn 等
- 异步的进行某些操作,尤其是CPU Bound操作
让网络请求、后台的密集计算等影响前台CLI程序与用户的使用
- Node通过child_process模块赋予了我们创造子进程的能力
cp.exec 、 cp.spawn
#shell.js(调用其他程序)
const shell = require('shelljs');
if(!shell.which('git')){
shell.echo('Sorry, this script requires git')
shell.exit(1)
}
shell.rm('-rf','out/Release')
shell.ls('*.js').forEach(function(file)=>{
shell.sed('-i','BUILD_VSRSION','v0.1.2',file)
})
shell.cd('..')
if(shell.exec('git commit -am "Auto-commit"').code !==0){
shell.echo('Error: Git commit failes')
shell.exit(1)
}
- 对bash命令提供了跨平台的封装
- 可以同步的获得命令结果
execa (调用其他程序)
const execa =require('execa');
(async ()=>{
const {stdout}=await execa()
console.log(stdout)
})()
const execa =require('execa');
execa('echo',['unicorns']).stdout.pipe(process.stdout)
- 结果promise化
- 跨平台支持Shebang
- 获取进程结束信号
- 优雅退出
- 更好的windows支持
#1.3.5 拆解CLI设计-以脚手架为例
- 需求描述
设计一个脚手架CLI,根据命令选择不同的模版,按指定的参数在指定的路径生成一个样板工程
- 拆解需求
- 参数的输入,结果的输出 commanderjs、inquirer、chalk
- 模版在哪里维护 git 仓库维护模版
- 如何让获取模版 git clone,使用execa或shelljs调用
- 如何根据模版
模版引擎,例如handlebars
#1.3.5 脚手架似乎是有套路的
如果想快速开发脚手架,那就用脚手架的框架Plop、yeoman-generator;脚手架一系列封装。
#1.3.6 革命性的脚手架-Schemetics
- 配合schematics-utilities 可以做到语法级别的样板代码生成
- 可以引入虚拟文件系统,可以保证写入原子性
- 支持多个Schematics之间的组合和管道
- 文档还不完善
#扩展阅读
- ink
用React开发CLI应用
. 专注于CLI的视图层 . 利用React的平台无关性(更换renderer)
- oclif
从工程角度封装CLI开发的复杂性
- 提供Plugin机制,便于扩展
- 提供预定义的生命周期
- 更紧凑的工程结构
二、规范先行
2.1 代码规范
#2.1.1 社区已有的规范
#HTML/CSS
- Google HTML/CSS/JS 规范 著名的谷歌前端规范,大二全
- AIrbnb Style 规范(包括CSS和Sass) AIrbnb的样式规范,不仅包含css规范,亦包含Sass的规范
#javaScript 规范
- Airbnb javaScript规范 Airbnb的javascript编码规范
- javascript Standard Style Standard规范,影响力最大的JS编码规范,生态丰富,提供了开箱即用的各种lint规则和编辑器插件
#框架相关
- Vue style Guide VueJS官方推荐的编码规范
- Airbnb React/JSX Style Guide Airbnb javascript规范的React/JSX部分
#2.1.2 建立代码规范 - ESLint
-
Eslint介绍 一款高度可配置的javaScript静态代码检测工具,已经成为js代码检查的事实标准
-
特效
- 完全的可插拔,一切行为都通过配置产生
- 任意rule之间都是独立的
-
原理 先通过解析器(parser)将javaScript代码解析为抽象语法树(AST),再调用规则对AST进行检查,从而实现对代码的检查
-
AST 浅析 AST是一种可遍历的、描述代码的树状结构,利用AST可以方便地分析代码的结构和内容astexplorer.net/
-
ESLint CLI
eslint -h
-
CLI 之外
- 编辑器的集成 VS Code/Atom/Vim/Sublime Text 在写代码的同时就可以实时对代码进行检查
- 构建工具集成 Webpack/Rollup/Gulp/Grunt 在构建过程中进行代码检查
🍅 ESLint 的配置
-
配置文件格式 javascript,JSON或者YAML,也可以在package.json中的eslintConfig字段
-
ESLint配置的主要内容
- Parser:ESLint使用哪种解析器
- Environments:选择你的代码跑在什么环境中(browser/node/commonjs/es6/es2017/worker)
- Globals:除了Env之外,其他需要额外指定的全局变量
- Rules:规则
- Plugins:一组以上配置项以及processor集合,往往用于特定类型文件的代码检查,如.md文件
- Extends:你想继承的配置
#parser配置
{
"parser":"esprima",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
}
}
- parser 指定ESlint使用哪种解析器:Espree(type默认)、Esprima、Babel-ESLint、@typescript-eslint/parser,一般不需指定
- parserOptions 配置parser的参数,parser会接收这些参数,并影响其解析代码的行为
#Evironments、Globals
{
"env":{
"browser":true,
"node":true
},
"globals":{
"var1": "writable",
"var2": "readonly",
"var3": "off"
}
}
-
Environments 预置环境 browser/node/commonjs/shared-node-browser/es6/es2017/es2020/worker/amd
-
globals globals是env之外需要额外指定的全局变量,有三种配置值:
- writeable - 可写
- readonly - 只读
- off - 不支持
#Rules
ESLint无默认开启规则,但提供了推荐开启的规则:"extends":"eslint:recommended",可在这里查看所有内置规则的列表:Rules
{
"rules": {
// 允许非全等号
"eqeqeq": "off",
// 尽可能使用花括号
"curly": "error",
// 双引号
"quotes": ["error","double"],
// 除了warn和error之外,对console.*方法发出警告
"no-console": ["warn", { "allow": ["warn","error"]}],
// 必须写分号,除了lastInOneLineBlock
"semi": [2, "always", {"omitLastInOneLineBlock":true}],
// plugin1 中的规则,不是内置规则
"plugin1/rule1": "error"
}
}
-
错误级别
- "off"或0 关闭规则
- "warn"或1 将规则视为一个警告
- "error"或2 将规则视为一个错误
-
配置形式
- 值:数字或字符串,表示错误级别
- 数组:第一项是错误级别,之后的各项是对该规则的额外的配置
#Plugins
ESLint的插件是对一系列rules、environments、globals、processors等配置的封装,以eslint-plugin-vue为例:
// eslint-plugin-vue 的入口文件index.js
// 这个配置集成好了一些配置,用户如果有需要可以直接继承它,不需要额外指定
module.exports = {
rules: {
'array-bracket-newline': require('./rules/array-bracket-newline'),
'array-bracket-spacing': require('./rules/array-bracket-spacing'),
'arrow-spacing': require('./rules/arrow-spacing')
// ......
},
config: {
base: require('./configs/base'),
essential: require('./configs/essential'),
'no-layout-rules': require('./configs/no-layout-rules'),
recommended: require('./configs/recommended')
// .....
},
// processors 在被ESLlint处理之前都会被eslint-plugin-vue处理一便
processors: {
'.vue': require('./processor')
}
}
使用方式
- 可以单独引用规则
- 可以直接使用(继承)eslint-plugin-vue配置好的config
- 预处理器的作用:解析.vue文件
#Plugins的使用
使用eslint-plugin-vue的vue工程为例子
module.exports= {
root: true,
env: {
node: true
},
extends: [
"plugin:vue/essential", // eslint-plugin-vue
"eslint:recomended",
"@vue/prettier"
],
parseroption: {
parser: "babel-eslint"
},
rules: {
"no-console": process.env.NODE_ENV === "production"
? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production"
? "warn" : "off"
}
}
更灵活的配置:
{
"plugins": [
"vue", // eslint-plugin-vue
"html"
],
"rules": {
"vue/no-unused-vars": "error",
"vue/array-bracket-spacing": "error"
}
// ...
}
extends和plugins的区别,extends是全家桶,继承插件的全部配置;plugins是DIY,是根据自己的情况在Rules里进行配置。
#Extends
Extends是一种非常灵活的ESLint配置机制,使用Extends可以依次递归地应用每一个eslint配置文件,实现灵活的组合
{
// 继承单个配置
"extends": "eslint:recommended",
// 继承多个配置,后面的可能覆盖前面的
"extends": ["eslint:recommended","plugin:react/recommended"],
"extends": [
"./node_modules/coding-standard/eslintDefaults.js",
"./node_modules/coding-standard/.eslintrc-es6",
"./node_modules/coding-standard/.eslintrc-jsx"
]
}
- 可以用extends来全家桶式地使用第三方配置好的规则
- extends可以嵌套
- 使用extends之后,我们的rules可以覆盖重写第三方规则、只改变第三方规则的错误等级、添加新的规则
2.1.3 编写自己的ESLint规则
规则:no-caller,禁止argutments.caller和arguments.callee的使用
- meta部分主要包括规则的描述、类别、文档地址、修复方式以及配置下schema等信息
- create则需要定义一个函数用于返回一个包含遍历规则的对象,并且该函数会接收context对象作为参数
- ESLint开发指南
module.exports= {
meta:{
type:"suggestion",
docs: {
descripttion: "disallow the use of `arguments.caller`"+"or `arguments.callee`",
category:"Best Practices",
recommended: false,
url: "http://eslint.org/docs/rules/no-caller"
},
schema: [],
messages: {
unexpected: "Avoid arguments.{{prop}}"
}
},
create(context) {
return {
MemberExpression(node){
const objetName = node.object.name,
propertyName= node.property.name;
if(objectName === "arguments" &&
!node.computed && // 必须是静态的属性访问方式a.b而不是a[b]
propertyName &&
propertyName.match(/^calle[er]$/u)
){
context.report({ // context eslint全局上下文,report输出错误日志
node, // 出错的节点
messageId: "unexpexted", // 报错的提示信息
data : { prop : propertyName} // prop 和meta中的message结合渲染出正确的提示信息
})
}
}
}
}
}
🍅 案例:检查class是否包含constructor构造方法
利用这个网站astexplorer比较有constructor和没有constructor的变化,然后劫持ClassDeclaration 看里面的节点是否有MethodDefinition和kind是不是constructor
// no-constructor.js
module.exports ={
meta: {
docs: {
description: "required class constructor",
category: "Best Practices",
recommended: true
},
fixable: null,
schema: []
},
create: function(context){
return {
ClassDeclaration(node){
const body = node.body.body;
const result = body.some(
element => element.type === 'MethodDefinition' && element.kind === 'constructor'
)
if(!result){
context.report({
node,
message: 'no constuctor found'
})
}
}
}
}
}
- meta部分
- create部分-在什么时机价差?-ClassDeclaration
- create部分-怎么检查?-遍历AST
- 怎么知道AST的结构呢?astexplorer
完整代码可以查看项目根目录/examples/engineering/2.1/coding-standards
#2.1.4 Stylelint 介绍
Stylelint是目前生态最丰富的样式代码检查方案,主要有如下特点:
- 社区活跃
- 插件化,功能强大
- 不仅支持css,还支持scss、sass和less等预处理器
- 已在Facebook、GitHub和WordPress等大厂得到广泛应用
#2.1.5 建立代码规范- Prettier
- prettier是啥?
一个流行的代码格式化的工具
- 为什么需要Prettier
- Prettier称自己最大的作用是:可以让大家停止对“代码格式”的无意义的辩论。
- Prettier在一众工程化工具中非常特殊,它毫不掩饰地称自己是“有主见的”,且严格控制配置项的数量,它对默认格式的选择,完全遵循
让可读性最高这一标准 - Prettier认为,在代码格式化方面牺牲一些灵活性,可以让开发者带来更多的收益,不得承认Prettier是对的。
#Prettier VS Linters
Prettier认为lint规则分为两类
- 格式优化类:max-len、no-mixed-spaces-and-tabs、keyword-spacing、comma-style
- 代码质量类:no-unused-vars、no-extra-bind、no-implicit-globals、prefer-promise-reject-errors
prettier只关注第一类,且不会以报错的形式告知格式问题,而是在允许开发者按自己的方式编写代码,但是会在 特定时机(save、commit)将代码格式化 为可读性最好的形式
🍅 Prettier的配置
// .prettierrc
{
"parser": "babylon", //使用parser
"printWidth": 80, // 换行字符串阀值
"tabWidth": 2, // 缩进空格数
"useTabs": false, // 使用空格缩进
"semi": true // 句末加分号
//......
}
🍅 Prettier使用
在很多方式去触发Prettier的格式化行为:Cli、Watch Changes、git hook 与linter集成
- Watch Changes
// package.json
{
"script": {
"prettier-watch": "onchange '**/*.js --prettier --write {{changed}}"
}
}
#与ESlint集成
yarn add --dev eslint-config-prettier eslint-plugin-prettier
eslint-config-prettier : 禁止eslin中与prettier相冲突的规则,当eslint与prettier相冲突时,eslint的规则不会报错。 eslint-plugin-prettier:让eslint以prettier的规则去检查代码,格式化的代码全部听prettier。
// .eslintrc.json
{
"extends": ["plugin:prettier/recommended"],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error"
}
}
2.2 版本规范和Changelog
#2.2.1 npm包的版本
#Semantic Versioning
前面的是正式版本,后面的是pre-release版本
- major
主版本,一般代表这Breaking Change,例如vue1.x和vue2.x、webpack 3.x和webpack4.x
- minor
次版本,一般代表着新的feature的出现
- patch
一般不包含新功能,知识bugfix或和功能关系不大的修改
-
pre-release
- alpha
- beta
- ...
- rc(release candidate)
预发行版本,一般用于正式版发行前的验证、联调和测试,和正式版本好之间用-连接
- 大小比较
2.3.2 > 2.2.17 > 2.2.17-beta.1 > 2.2.17-beta.0 > 2.2.17-alpha.1 > 2.2.16
#版本范围
// 1. - 表示范围,边界可等
- 案例: 1.2.3-2.3.4 // 大于等于1.2.3 小于等于 2.3.4
// 2. x 表示通配,和各种语言的通配符一样
x 案例:1.2.x // 大于等于1.2.0 小于1.3.0
// 3. ~ 表示限制minor版本的升级
~ 案例:~1.2.3 // 大于1.2.3 小于1.3.0
// ^ 表示允许第二个非零的版本的升级
^ 案例:^1.2.3 // 大于1.2.3 小于2.0.0
^ 案例:^0.2.3 // 大于0.2.3 小于0.3.0
^ 案例:^0.0.3 // 大于0.0.3 小于0.0.4
#为什么我们要遵循Semantic Versioning ?
- 为了让我们的版本语义和npm社区统一,可以让我们的npm包可以正确的被用户使用
- 享受社区生态带来的遍历,让我们可以利用社区现有的方案,更灵活的管理依赖的版本
#2.2.2 changelog
- 什么是changelog?
Changelog是以时间为倒序的列表,记录所有版本的重大变化
- 为什么要有Changelog?
为什么让我提供库和框架的用户了解每个版本发生了哪些变化,提供多于版本号的信息
#2.2.3 自动化的npm包版本控制和Changelog
-
release-it
- 根据git commit自动生成版本号
- 自动生成Changelog
- 丰富的hooks用来定制发版逻辑
- 提供插件机制,高度可扩展
release-it 配置文件
{
"hooks": {
"after:bump": "auto-changelog -p"
},
"git": {
"changelog": "auto-changelog --stdout --commit-limit false",
"requireCleanWorkingDir": false,
"requireUpstream": true,
"requireCommits": false,
"addUntrackeFiles": false,
"commit": true,
"commitMessage": "version release ${version}",
"commitArgs": "",
"tag": true,
"tagName": "${version}",
"tagAnnotation": "Release ${version}",
"taArgs":"",
"push":true,
"pushArgs": "--follow-tags",
"pushRepo": "origin"
},
"npm": {
"publish": true,
"publishPath": ".",
"access": null,
"otp": null
},
"plugins": {
"@release-it/conventional-changlog": {
"preset": "angular",
"infile": "CHANGELOG.md"
}
}
}
package.json
{
"script" : {
"release": "release-it",
"release:alpha": "release-it --preRelease=alpha",
"release:beta": "release-it --preRelease=beta"
}
}
案例:
git add .
git commit -m"feat: update xxx"
yarn release
# 一路回车
#扩展学习资料
.npmrc-npm的配置文件
由于国内网络问题,部分常用的二进制依赖下载速度较慢,为了加速二进制依赖下载,我们可以利用.npmrc配置国内的镜像源:
ELECTRON_MIRROR = "npm.taobao.org/mirrors/ele…" electron
PHANTOMJS_CDNURL = "npm.taobao.org/mirrors/pha…" phantomjs
SASS_BINARY_SITE = "npm.taobao.org/mirrors/nod…" node-sass
三、质量代码
3.2 测试基础
#3.2.1 测试的基本概念
#测试框架(jest)
- 开箱即用 基本不需要额外的配置即可使用
- 功能强大 自带断言、测试覆盖率工具,支持Mock、Snapshot、异步测试等
- 应用广泛 已经成为vue cli和create-react-app默认集成的测试框架
- 文档丰富 jest
#test suit / test case
Test Case
测试用例,表示对一个功能点的测试; 在jest中他是一个名为test的函数,第一个参数它是test case 的名称,第二个参数是一个实际执行的函数;这个函数 没有报错才能算这个测试用例通过了
Test Unit
测试套件,表示一组相关用例的分组; 在jest中一个名为describe的函数,第一个参数是Test suit的名称,第二个参数是一个实际执行的函数;Test case(测试用例)应该在 这个函数的函数体中,test Unit本身没有什么逻辑,它只是把用例分组,这样无论是编写还是在生成测试报告的时候都会使我们的工作显得更加有条理性。
const myBeverage={
delicious: true,
sour: false
}
// test unit
describe('my beverage',()=>{
// test case
test('is delicious',()=>{
expect(myBeverage.delicious).toBeTruthy()
})
test('is not sour',()=>{
expect(myBeverage.sour).toBeFalsy()
})
})
#3.2.2 断言
#断言(assertion)是什么?
- 在程序设计中,断言 是一种一阶逻辑,目的是表示与验证开发者预期的结果。
- 当程序运行到断言的位置时,对于的断言应该为真;若断言为假时,程序会终止并给错误信息。
#js中的断言
- 浏览器:console.assert (看起来很像断言,但只输出错误信息不报错,所以我们不认为它是一个真正的断言)
- Node:assert模块 (真正的断言)
- 第三方自定义:例如Vux
function assert (condition,msg){
if(!condition){
throw new Error(`[Vuex]${msg}`)
}
}
#jest中的断言
jest中内置了很多断言方法,不过称作“Matcher”
#Matcher的使用
// 测试data这个对象
test('object assignment',()=>{
const data = {one : 1};
data['two']=2;
// 预测data这个对象是否与toEqual传入的对象相等
expect(data).toEqual({one:1,two:2})
})
// expect(something).toBe(something) 这里的toBe是具体的判断方法
jest中的Matchers,包含各种数据类型的各种判断方法
更多Matchers:传送门
test("null",()=>{
const n = null;
expect(n).toBeNull()
expect(n).toBeDefined()
expect(n).not.toBeUndefined()
expect(n).not.toBeTruthy()
expect(n).toBeFalsy()
})
test('two plus two',()=>{
const value = 2+2
expect(value).toBeGreaterThan(3)
expect(value).toBeGreaterThaOrEqual(3.5)
expect(value).toBeLessThan(5)
expect(value).toBeLessThanOrEqual(4.5)
expect(value).toBeLessThanOrEqual(4.5)
expect(value).toEqual(4)
})
3.2.3 异步测试
测试中难免遇到异步逻辑,jest也提供了异步测试支持
下面的用例可以得到预期的结果吗?
test("the data is peanut butter",()=>{
function callback (data) {
expect(data).toBe('peanut butter')
}
fetchData(callback)
})
上面错误案例❌
- jest会认为这是一个同步的用例
- 在回调执行之前用例就执行结束了
test("the data is peanut butter",done=>{
function callback (data) {
try{
expect(data).toBe('peanut butter')
done()
}catch(error){
done(error)
}
}
fetchData(callback)
})
测试异步回调的正确姿势,利用done参数,传递错误
promise和async/await场景下的异步测试
test('the data is peanut butter',()=>{
return fetchData().then(data=>{
expect(data).toBe('peanut butter')
})
})
test('the data is peanut butter',()=>{
await expect(fetchData()).resolves.toBe('peanut butter')
})
#3.2.4 环境准备
有些时候我们在测试之前,要做一些环境准备的工作,在测试之后把环境清理恢复到初始状态
- beforeEach(每个用例之前)
- afterEach(每个用例之后)
- beforeAll (所有的用例之前)
- afterAll (所有的用例之后)
// 在该文件中的所有test运行之前,都会执行
beforeEach(()=>{
return initializeCityDatabase()
})
test('city database has Vienna',()=>{
expect(isCity('Vienna')).tiBeTruthy()
})
describe('matching cities to foods',()=>{
// 只会在这个test suit(套件)的每个test前运行
beforeEach(()=>{
return initialzeFoodDatabase()
})
test('Vienna<3 sausage',()=>{
expect('Vienna<3 sausage',()=>{
expect(
isValidCityFoodPair('Vienna','Wiener Schnitzel').toBe(true)
)
})
})
})
3.2.5 Mock
Mock是一种覆盖原有函数、类的实际实现,来检测其调用情况的一种测试方法
const mockCallback= jest.fn(x=>42 + x)
forEach([0,1],mockCallback)
// mockCallback被调用2次
expect(mockCallback.mock.calls.length).toBe(2);
// mockCallback的第一次调用的第一个参数应该是0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// mockCallback的第二次调用的第一个参数应该是1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// mockCallback的第一次调用的结果应该是42
expect(mockCallback.mock.results[0].value).toBe(42);
可以通过Mock函数的.mock属性,拿到Mock函数调用的各种信息
Mock 一个第三方模块
// usesr.is
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json')
.then(resp=> resp.data)
}
}
export default Users
mock原有的axios,这样不依赖网络也可以测试
// uses.test.is
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users',()=>{
const users = [{name:'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp)
return Users.all()
.then(data=> expect(data))
.toEqual(users)
})
#3.2.6 Snapshot
快照是一种非常强大的测试工具,一般用于UI组件的测试
import React from 'react';
import Link from '../Link.jsx';
import renderer from 'react-test-renderer';
it('renders correctly',()=>{
const tree = renderer
.cteate(
<Link page="http://www.facebook.com">
Facebook
</Link>
)
.toJSON();
expect(tree).toMatchSnapshot();
})
对于这个Link组件的测试用例,这个组件第一次运行的时候会生成快照,快照的结构如下图所示,从第二次开始jest都会把当前组件和以前生成的快照做对比; 看看他们是否吻合,快照测试相当于以某一版本的UI为基础,把这次的ui拍成一张照片放在那里;之后每次测试都以当前的ui和之前的照片做对比,如果不一致就 报错,快照测试可以防止我们无意中修改了组件,由于快照在第一次才会拍下,如果想改组件必须手动修改快照,在jest中可以用__updateShapshot修改。
exports[`renders correctly 1`] =`
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[function]}
onMouseLeave={[function]}
>
Facebook
</a>
`
第一次:生成快照 第2...N次:使用快照对比现有组件
#3.2.7 测试覆盖率
我们用测试覆盖率衡量一个工程的测试代码的完整程度
jest内置了istanbul模块,可以从以下4个纬度统计测试覆盖率“
- statements 语句覆盖率 所有语句的执行率 (重要)
- Brancher 分支覆盖率 所有代码分支如 if、三目运算的执行率 (重要)
- Functions 函数覆盖率 所有函数的被调用率
- Lines 行覆盖率 所有有效代码行的执行率和语句类似,但是计算方式略有差别
3.6 监控和异常上报
#3.6.1 初识监控
#异常捕获
- 常见的捕获方式
// 浏览器
window.onerror //全局异常捕获
window.addEventListener('error') // js错误、静态资源加载错误
window.addEventListener('unhandledrejection') //没有catch的Promise错误
// node端
process.on('uncaughtException') // 全局异常捕获
process.on('uncaughtException') // 没有catch的Promise错误
- 利用框架、三方库本身能力
- Vue.config.errorHandler
- React.ErrorBoundary
#3.6.2 Sentry监控
#简单介绍
- 知名的开源监控方案
- 不仅可用于前端,也可以用于后端应用
- 开箱即用的错误收集、错误上报、数据分析、监控告警功能
#优化点
数据爆炸
解决方案:
前端监控的一个显著特点:容易短时间产生海量数据
- 采集端考虑:采样后上报(sampleRate)将采样率设置为0.1,那么它只上报十分之一的错误;错误发生10次,只会上报一次。
- 手机端考虑:生产者消费者模式-利用消息队列控制消息消费速度。
效果增强
- 更多的告警方式。
- 收集用户反馈,获得用户的主观意见和建议。
- 更丰富的上下文信息:用户操作录像等(rrweb)。
四、工程设计
4.1 工程设计范式
- 工程设计范式有哪些种类
Rails Style / Domain Style
-
相信很多有一定经验的开发者都会遇到选择这种问题
- 一个看似分层良好的古老工程,随着业务发展却越来越臃肿和低效
- 一个新的项目,因为这种隐形问题上的纠结,而耽误了自己时间和精力
#4.1.1 工程范式分类
#🍅 Rails Style
// egg应用典型结构
|── app
| ├── config
| ├── controller
| ├── extend
| ├── public
| ├── router.ts
| ├── service
| └── view
├── app.ts
├── agent.ts
├── config
├── logs
├── test
├── typings
├── README.md
├── package.json
└── yarn.lock
// Rails Style TodoList
root
|── reducers
| ├── todoReducer.js
| └── fileterReducer.js
├── actions
| ├── todoActions.js
| └── filterAction.js
├── components
| ├── todoList.jsx
| ├── todoItem.jsx
| └── filter.jsx
├── containers
| ├── todoListContainer.jsx
| ├── todoItemContainer.jsx
| └── filterContainer.jsx
├── App.jsx
└── index.js
-
Rails Style的特点
- 专注于纵向的“层”的划分
- 同一类文件放置在同一目录下
-
优势:
- 便于合并导出
- 便于进行“层”的扩展
-
不足:
- 依赖关系难以直接地分析
- 对功能的修改会涉及到大量的目录切换
- 难以水平拆分
🍅 Domain style
// Aant Design
├── ...
├── components
| ├── util
└── ...
| ├── alert
| | ├── demo
| | ├── index.en-US.msd
| | ├── index.tsx
| | ├── index.zh-CN.md
| | └── style
| ├── anchor
| | ├── Anchor.tsx
| | ├── AnchorLink.tsx
| | ├── __tests__
| | ├── demo
| | ├── index.en-US.msd
| | ├── index.tsx
| | ├── index.zh-CN.md
| | └── style
└── ...
-
Domain style的特点
- 专注于横向的 “功能” 的划分
- 同一个feature放置在同一目录下
-
优势:
- 便于水平拆分
- 便于进行“功能”的扩展
-
不足
- 会产生大量的重复结构
- 难以垂直拆分
#4.1.2 如何选择工程范式
-
单一功能的项目
- 库、三方包:fs-extra、axios等
- 由于不存在水平拆分的必要性、故可以选择Rails Style
- 易于扩展
- 减少重复代码
// Axios
root
|── dist
| └── ...
├── examples
| └── ...
├── lib
| ├── adapters
| | └── ...
| ├── cancel
| | └── ...
| ├── core
| | └── ...
| ├── defaults.js
| └── axios.js
├── sandbox
| └── ...
├── test
| └── ...
├── package.json
└── webpack.config.js
-
聚合功能型项目
- 组件池、utils:ant-design、element-ui、lodash等
- 纵向分层少,极易横向扩展-故选择Domain Style
- 易于添加新feature
- 便于横向拆分
-
业务工程项目
- 即有大量的垂直分层,又有大量的feature聚合
- Rails Style + Domain Style
#🍅 Rails Style 对比 Domain Style
| 工程设计范式 | 特点 | 优势 | 不足 | 适用项目 |
|---|---|---|---|---|
| Rails Style | 纵向“层”的划分,同一类文件放置在同一目录下 | 便于合并导出,便于进行“层”的扩展 | 依赖关系难以直观地分析,对功能的修改会涉及到大量的目录切换 ,难以水平拆分 | 单一功能的项目 |
| Domain Style | 横向“功能”的划分,同一feature放置在同一目录下 | 便于水平拆分,便于进行“功能”的扩展 | 会产生大量的重复结构,难以垂直拆分 | 聚合功能型项目 |
业务工程项目一般需要Rails Style 和 Domain Style结合
4.2 multi-repo VS mono-repo
它们是什么?
这是两种代码风格仓库的管理风格
- multi-repo:把每个项目都分别用git托管
- mono-repo:统一用一个git仓库管理所有的项目
#4.2.1 multi-repo
// multi-repo
root
|── project-a
| ├── ...
| └── .git
├── project-b
| ├── ...
| └── git
├── project-c
| ├── ...
| └── .git
├── project-d
| ├── ...
| └── .git
...
从上面可以看出多个项目对应多个仓库,大多数工程,其实都是以multi-repo方式管理的
-
优势:
可以让各项目团队根据需要定制更适合自己的workflow
-
不足:
- 难以对所有项目统一进行操作(git checkout / npm publish / npm run build)
- 难以追踪依赖关系(a->b->c)
#4.2.2 mono-repo
// multi-repo
├── .git
├── lerna.json
├── package.json
├── packages
├── project-a
| ├── README.md
| ├── __tests__
| ├── lib
| └── package.json
├── project-b
| ├── README.md
| ├── __tests__
| ├── lib
| └── package.json
├── project-c
├── README.md
├── __tests__
├── lib
└── package.json
广泛应用于一些知名开源项目和硅谷巨头(React/Angular/Vuetify/Google)
-
优势
- 方便统一地操作各个项目(git checkout / npm publish / npm run build)
- 利用工具,可以方便地追踪项目间的依赖关系
-
不足:
- 代码库随着业务发展会非常巨大
- 失去部分的灵活性(workflow必须统一)
- 强依赖于mono-repo的管理工具
#4.2.3 multi-repo的管理
- git submodule
# 初始化git submodules仓库
git submodule init
# 添加一个submodule
git submodule add https://github.com
# 更新所有的submodule
git submodule update
# 查submodule status
git submodule status
# foreach 用于在每个submodule中执行命令
git submodule foreach "git checkout -b featureA"
- git提供的一种管理子仓库的方案
- 可以批量管理多个git repo
- 本质上是一个父repo维护了一份各个子repo的清单
- 坑还是不少的:git Submodule的坑
- 替代方案:git subtree
#4.2.4 mono-repo的管理-lerna
- 为js生态下的mono-repo管理提供一站式的解决方案
- babel/create-react-app/jest/react-router/umi/nestjs
解决方案:
mono-repo下的依赖管理、版本管理、开发提效、工作流
#目录结构
- 整体作为一个Git仓库,也是个npm包(私有)
- lerna.json是整个mmono-repo的配置文件
- 每个真正的项目,平铺在packages/中
- 整个项目可以统一管理所有依赖(也可以分别管理)
// lerna
├── .git
├── lerna.json
├── package.json
├── packages
├── project-a
| ├── README.md
| ├── __tests__
| ├── lib
| └── package.json
├── project-b
| ├── README.md
| ├── __tests__
| ├── lib
| └── package.json
├── project-c
├── README.md
├── __tests__
├── lib
└── package.json
#用lerna管理项目实战 重要
用lerna管理项目实战
npm i lerna -g
lerna init
- 初始化的目录结构
- packages
- lerna.json
- package.json
# 创建项目1
lerna create pac-1
# 创建项目3
lerna create pac-2
# 创建项目3
lerna create pac-3
目录中 lerna.jsoon
{
// 配置$schema可以在vscode中,鼠标滑倒每个配置项时候,可以看每个配置项的介绍
// 可以在https://www.schemastore.org/json/中网站查看知名项目的描述文件
"$schema": "http://json.schemastore.org/lerna",
"packages": [
"packages/*"
],
"version": "independent" // 可以给各个项目发不同的版本
}
TIP
接下来我们为pac-1、 pac-2、pac-3添加依赖关系,我们制造这样一种依赖,pac-1模块被pac-2依赖;pac-2模块被pac-3模块依赖, 这时候就形成了pac-1、 pac-2、pac-3间接依赖关系。
- 添加项目依赖
# 制造依赖关系,对于内部项目的依赖,lerna会以软连接的形式,给它们相互软链接起来
lerna add pac-1 packages/pac-2
lerna add pac-2 packages/pac-3
如果我们给项目添加外部依赖,可能需要给每个项目node_moudles都添加,同一个依赖会安装很多次,这显然不合理,为了解决这个问题,leran可以把依赖添加到项目根目录;lerna clean去清除每个项目中node_modules相同的依赖;用 lerna bootstrap --hoist重新安装依赖,
- 发版
需要注意是的 lerna.json 配置为"version": "independent"时可以为每个包独立发不同的版本,如果"version":'0.0.0'是发同样的版本
leran publish
- 其它
可以用lerna -h查看全部指令
lerna exec [cmd] [args...] # 执行每一个package.json你想指行的命令
lerna run <script> # 执行package.josn 中script配置的命令
lerna diff [pkgName] # 对比 文件变化
lerna changed # 查看文件变化
#lerna.json 配置
{
"version":"0.0.0",
"npmClient":"npm",
"npmClientArgs":[
"--pure-lockfile"
],
"command": {
"publish": {
"ignoreChanges": ["ignored-file","*.md"],
"message": "chore(replease):publish",
"registry": "https://npm.pkg.github.com"
},
"bootstrap": {
"ignore": "component-*",
"npmClientArgs": ["--no-package-lock"]
}
},
"packages": ["packages/*"]
}
-
其实只有几个主要字段:version、npm*、command、packages
-
command配置-json schema查看
- 配置lerna子命令的默认项目
-
version: 决定fixed mode / independent mode
-
packages:项目路径
-
npm*:解耦包管理工具
一些注意项
- mono-repo 不可嵌套
- mono-repo 的主仓库必须是私有的(private:true)
- 如果你对仓库的私密性要求非常高,甚至可以不用npm而使用git应用依赖
- 任何的json配置记不住,都可以用json schema
源码地址:/examples/engineering/4.1/lerna-demo
#lerna 外的选择-nx
- Angular出品
- 框架强相关:Angular、React
- 支持插件机制
- 远超mono-repo管理的强大功能
#扩展资料
五、构建艺术
.1 构建简史
#5.1.1 前端进化史
#洪荒时代
- Vanilla javascript/HTML/CSS
1993年-HTML(超文本标记语言)-> 1994-css(层叠样式表)-> javaScript
- 最火热的话题
DOM、BOM、样式放在哪、浏览器的兼容性.....
- jQuery
- 简化DOM操作
- 制作底层API(如xhr)
- 制作炫酷动画
- 解决浏览器兼容性问题
- Bootstrap
- 用class决定元素的功能和样式
- “组件”开始出现
- 工程化的问题开始显现
#曙光初现
- Nodejs出现
- NodeJS v0.0.1-2009年
- 利用v8和libuv,让JS代码运行于浏览器之外
- Node出现之前,构建脚本往往需要使用Makefile、Shell编写
var sass=require('node-sass')
sass.render(
{file:scss_filename,[,option...]},
function(err,result){/*...*/}
)
#百家争鸣
- Grunt/Gulp
- 构建工具流开始初步形成
- 编写简单、生态丰富
#语言的进化
- ECMAScript 6的出现
- 即ES2015,诞生于2015年6月
- 现代的语法极大的提高了开发效率
- 箭头函数
- Clas语法
- Promise/Gennerator
- ES Module ......
#MV*框架
- 现代mv*框架的出现
- Angular、React、Vue
- 单文件组件、JSX
- 大量利用了ES6新特性
#MV*框架
- AMD/CMD
- 纯前端模块化方案
- AMD:require.js
- CMD:sea.js
- CommonJS
- NodeJS模块化方案
- 同步引用依赖、符合人类视觉
module.exports=function(){
return 'a'
}
var a=require('./a')
- ES Module
- ES6规带来的语言级模块化方案
- 支持Node/Browser等运行时
- 利于静态分析
export default function (){ return 'a' }
import { default as a } from './a'
#5.1.2 现代化的前端构建
- 我们需要怎样的前端构建
- 性能:图片优化、合并资源、减少Polyfill体积
- 模块化:Commonjs/ES Module-> script
- 强力的语法转换:ES6、7、8...
- 统一打包过程、整体分析优化:Vue单文件组件
- Babel、webpack
5.2 不得不提的babel:token-ast
#5.2.1 回顾AST
#《代码规范》中的介绍
AST是一种可遍历的、描述代码的树状结构,利用AST可以方便的分析代码的结构和内容。
#5.2.2 编译理论
#Babel中的编译
-
Babel也是编译器
输入的是高版本的ES代码,输出的是符合我们要求的低版本的ES代码,例如:ES7->ES5
-
Babel的工作步骤
根据Babel文档,其工作步骤其实主要分为三步
- 解析(Parsing):解析代码,生成AST(抽象语法树)
- 变换(Transformation):操作AST(抽象语法树),修改其内容
- 生成(Code Generation):根据AST(抽象语法树)生成新的代码
5.2.3 如何实现简单编译器
#目标
- LISP->C
| LISP | C | |
|---|---|---|
| 2+2 | (add 2 2 ) | add(2,2) |
| 4-2 | (subtract 2 2 ) | subtract(4,2) |
#parsing
- Tokenizing
- Tokenizer函数
将代码转换成token
- Parser函数
将token转换为AST
#transformation
- Traverser函数
深度优先地遍历AST树
- TransFormer函数
在遍历每一个节点时调用将旧AST转成一颗新树,就是转换为目标语言的树
#Code Generator
- Code Generator
深度优先地遍历新的AST树,将每个节点依次组合新代码
- 最终的Compiler
- input -> tokenizer -> tokens
- Tokens -> parser -> ast
- ast -> transformer -> newAst
- newAst -> generator -> output
function compiler (input) {
let tokens= tokenizer(input)
let ast = parser(tokens)
let newAst=transformer(ast)
let output= codeGennerator(newAst)
return output
}
#5.2.4 扩展资料
#5.3 不得不提的babel:基本概念
#5.3.1 Babel的作用
Babel是啥?
- Babel 是啥?
- Babel is javaScript compiler
- 主要将ECMAScript 2015+的代码,转换成让我们能够在更古老的浏览其和其他环境运行的、兼容性更好的、老版本javascript代码
- Babel 能干嘛?
作用1: 语法转换
[1,2,3].map((n)=>n+1) => [1,2,3].map(function(n){
return n+1
})
作用2: Polyfill
Array.from(new Set([1,2,3]))
[1,[2,3],[4,[5]]].flat(2)
Promise.resolve(32).then(x=>console.log(x))
让 老环境支持新的api
作用2: 源码修改
去除Flow/TypeScript代码中的类型标识
function square (n:number):number {
return n+n
}
// ------transformation------
function square (n) {
return n+n
}
5.3.2 Syntax & Feature
#Syntax
- Syntax
语言级的某一种概念的写法,不可被语言中的其他概念实现
举个例子
// 1. 箭头函数
(a,b,c)=>a+b+c
// 2. Class类
class A {}
// 3. ES模块
import * as ext from ‘fs-ext’
#feature
- Feature 就是指API
实例方法、静态方法、全局对象等
举个例子
// 1. promise
new Promise().then()
//2. Object.keys
Object.keys({a:1})
// 3. [].inculdes
[1,2,3].includes(2)
5.3.3 plugin / preset / env
#plugin
- 插件
babel本身不会对代码做任何操作,所有功能都靠插件实现
- 有哪些插件?
- @bable/plugin-transform-arrow-functions
- @babel/plugin-transform-destructuring
- @bable/plugin-transform-classes
- ......
#preset
- preset是什么?
A set of plugins,一组插件的集合
- 官方preset
- @babel/preset-env
- @babel/preset-flow
- @babel/preset-react
- @babel/preset-typescript
module.exports=function (){
return {
plugins:[
"pluginA",
"pluginB",
"pluginC"
]
}
}
#env
- env的出现
@bable/preset-env是一种更加智能的preset,让我们指需要根据我们的目标环境,快速配置babel
- env的配置例子
{
"target":">0,25%,not dead"
}
{
"target":{"chrome":"58","ie":"11"}
}
#5.3.4 扩展资料
5.4 不得不提的babel:使用
#5.4.1 Babel的使用方式
- 直接require
const bable=require("@babel/core")
babel.transform(code,options,function(){
result // =>{code,map,ast}
})
- babel-cli
babel src --out-dir lib --ignore "src/**/*.spec.js","src/**/* .test.js"
babel -node --inspect --presets @babel/preset-env -- script.js --inspect
- Webpack / Rollup
module:{
rules:[
test:/.m?js$/,
exclude:/(node_modules | bower_components)/,
use:{
loader:'babel-loader',
options:{
presets:['@bable/preset-env']
}
}
]
}
#5.4.2 Babel 的配置
#配置的位置
- 项目根目录的.babelrc.json
对整个项目生效
- 工程根目录的babel.config.json
对整个工程生效(可跨项目)
- package.json的babel字段
相当于.babel.json
#plugin
- plugin的使用
module.exports={
// "@babel/preset-env" ,下面配置的是简写,如果工程配置中找不到包,可能是被简写了
presets:["@babel/env"],
// same as "@babel/plugins-transform-arrow-functions"
plugins:["@babel/transform-arrow-function"]
}
- plugin的几种配置
// 以下三种配置方式等价
module.exports={
“plugins”:[
"pluginA":,
["pluginA"],
["pluginA",{}] // 如果plugin配置成数组,第一项是插件名称,第二项是配置
]
}
利用以下方式,我们可以将配置传入插件
module.exports={
"plugins":[
[
"transform-async-to-module-method",
{
"module":"bluebird",
"method":"coroutine"
}
]
]
}
- plugin的顺序
- Plugins在preset之前执行
- Plugin之间从前往后依次执行
babel为什么这么设计呢?
因为preset配置的是比较成熟的语法,plugin主要配置一些更新特性,plugin在preset之前执行是保证这些新特性是最先被转换的,保证preset只关心比较稳定的语法
preset
- preset的使用
{
"preset":[
["@bable/preset-env",{
"loose":true,
"modules":false
}]
]
}
为什么preset也需要配置呢?
因为preset本质就是一组plugin的集合,plugins可以配置,当然preset也可以配置,甚至preset可以依赖另一个preset
- preset的本质
module.exports=()=>({
presets:[
"@babel/preset-env"
],
plugins:[
[
"@babel/plugin-proposal-class-properties",
{loose:true}
],
"@babel/plugin-proposal-object-rest-spread"
]
})
- preset的顺序
- preset在plugin之后执行
- preset之间从后往前依次执行
// 执行顺序 c->b->a,这个设计babel文档中说是历史原因造成的
{
"preset":[
"a","b","c"
]
}
#preset-env
- preset-env的配置
preset-env是最常用的preset,大部分情况下你只需用这一个preset就可以了
- 主要就是useBuiltins和target两个配置
- useBuiltins用来配置polyfill
- target用来告诉preset-env选择哪个插件
{
"presets":[
[
"@babel/preset-env",
{
"useBuiltIns":"entry",
"target":{
"esmodules":true,
"chrome":"58",
"ie":"11",
"node":"current"
}
}
]
]
}
- targets的配置
这个配置项是我们支持的平台是什么
{
"targets" :{"chrome":"58","ie":"11"}
}
// or
{
"targets" : "> .5% and not last 2 versions"
}
- 可以是描述浏览器版本的对象,也可以是字符串(browserlist)
- browserlist完整语法
- 也可以将browserlist写在.browserslistrc中
- usebuiltins的配置
三种取值:“usage”、“entry”、“false”,默认是false
用于自动注入polyfill代码
- false:什么也不做/不自动注入polyfill
- entry:根据环境配置自动注入polyfill
- usage:根据实际使用自动注入polyfill
5.4.3 polyfill
#Babel的Polyfiill
- Babel 7.4之前
统一使用@babel/polyfill
- babel 7.4之后
新的形式更有利于babel做进一步的转换
import "core-js/stable";
import "regenerator-runtime/runtime"
core-js用于polyfill大部分的ES新feature
regenerator-runtime/runtime用于转换generator函数
由于polyfill会用于运行时,所以要以dependencies方式安装
#Polyfill的使用
- 直接引入?
官方不建议直接引入,因为太大了,建议将preset-env的useBuiltins和corejs搭配使用。
- useBuiltIns:“entry”
在target配置为chrome71的条件下使用:
import 'core-js/stable'
// ------------------------
import "core-js/modules/es.array.unscopables.flat";
import "core-js/modules/es.array.unscopables.flat-map";
import "core-js/modules/es.object.form-entries";
import "core-js/modules/web.immediate"
1
2
3
4
5
6
7
8\
- useBuiltins:false
Babel什么都不做,完全由你自己决定如何polyfill
- useBuiltins: "usage"
根据使用情况自动加入poilfill
// a.js
var set =new Set([1,2,3])
// 转换后
import 'core-js/module/es.array.iterator';
import 'core-js/modules/es.object.to-string';
import 'core-js/modules/es.set';
var set = net Set([1,2,3])
似乎完美了吗?
export class Animal {
makeSound(){
console.log('hi')
}
}
// ----------------------------
"use strict"
require("core-js/modules/es6.object.define-property")
function _classCallCheck(instance,constructor){//....}
function _defineProperties(target,props){//....}
function _createClass(Constructor,protoProps,staticProps){//....}
Polyfill函数被内联的写进文件里,如果工程中大量使用class语法,必然会出现大量的重复的polyfill
解决方法
yarn add -D @babel/plugins-transform-runtime
yarn add @babel/runtime
var _classCallCheck2=_interopRequireDefault(
require('@babel/runtime/helpers/classCallCheck')
)
var _classCallClass2=_interopRequireDefault(
require('@babel/runtime/helpers/createClass')
)
让所有polyfill函数从@babel/runtime引入
带来的好处
- 减小包的体积
- 不会影响到全局环境
最终配置
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage'
}]
],
plugins: [
"@babel/plugin-proposal-class-properties"
['@babel/plugin-transform-runtime'],
}
.5 不得不提的babel:插件开发
#5.5.1 Babel的插件的本质
#插件长什么样?
export default function (){
return {
visitor:{
Indentifier(path){
const name=path.node.name;
path.node.name=name
.split("")
.reverse()
.join("")
}
}
}
}
#从代码到AST
function square () {
return n*n
}
Babel和ESlint一样,使用EStree规范生成AST结构,可以使用AST Explore查看
#节点(Node)
- AST 每一层都拥有相同的结构,我们称之为节点(Node)
- 一个AST可以由单一的节点或成百上千个节点构成
- 它们组合在一起可以描述用于静态分析的程序语法
{
type:"FunctionDeclaration",
id:{.....},
params:[......]
body:{.....}
}
{
type:"FunctionDeclaration",
name:...
}
{
type:"FunctionDeclaration",
operator:......,
left:{......},
right:{...}
}
#遍历
babel编译经过3个步骤,解析->变换->生成;其中解析和生成我们都不用关注,我们只用关注变换,先要转换AST,我们 需要对其进行递归的树形遍历
{
"type": "FunctionDeclaration",
"start": 0,
"end": 37,
"id": {
"type": "Identifier",
"start": 9,
"end": 15,
"name": "square"
},
"expression": false,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "BlockStatement",
"start": 19,
"end": 37,
"body": [
{
"type": "ReturnStatement",
"start": 25,
"end": 35,
"argument": {
"type": "BinaryExpression",
"start": 32,
"end": 35,
"left": {
"type": "Identifier",
"start": 32,
"end": 33,
"name": "n"
},
"operator": "*",
"right": {
"type": "Identifier",
"start": 34,
"end": 35,
"name": "n"
}
}
}
]
}
}
- 从FunctionDeclaration 开始遍历
- id节点,它是一个identifier,没有任何子节点属性
- params数组,访问其中的任何一项,都是identifier
- body -> BlockStatement -> body
- ReturnStatement -> argument -> BinaryExpression
- ......
#访问者模式
- 遍历AST的过程,其实就是不断访问各个节点的过程
- Babel的插件,就是顺理成章地使用了访问者模式
const MyVisitor ={
Indentifier:{
enter(){
console.log("Entered")
},
exit(){
console.log("EXited")
}
}
}
访问者的每个方法都能获取2个参数,path和state
- path
path是我们对节点的引用
{
type:"FunctionDeclaration",
id:{
type:"Identifier",
name:"square"
}
......
}
// path拿到父节点
{
"parent":{
"type":"FunctionDeclaration",
"id":{}
.....
},
"node":{
"type":"Identifier",
"name":"square"
}
}
- path方法可以帮助我们访问父节点,帮助我们取得上下文信息。
- path方法上面有很多工具方法,帮助我们方便的操作AST。
- State
插件的“状态,比如: 当前plugin的信息、plugin传入的配置参数,甚至处理过程中的自定义状态
{
plugins:[
["my-plugin",{
"options":true,
"options":false
}]
]
}
// babel中通过state拿到配置参数
{
visitor:{
FunctionDeclartion(path,state){
console.log(state.opts)
// {option1:true,option2:false}
}
}
}
- 完整面貌
export default function (babel) {
// babel的一些工具方法
const {type:t,template}=babel
return {
name:"a-demo-plugin",
visitor:{
Indentifier(path,state){},
ASTNodeTypeHere(path,state){}
}
}
}
一个babel对象为入参,以包含插件名和visitor的对象为返回值的函数
#5.5.2 Babel的插件开发工具
| 工具 | 作用 |
|---|---|
| @babel/parser | 将源代码解析称AST |
| @babel/generator | 将AST生成js代码 |
| @babel/code-frame | 生成错误信息 |
| @babel/helpers | 提供一些内置的帮助函数 |
| @babel/template | 为parser提供模版引擎 |
| @babel/types | 主要用于处理节点类型相关的问题(判断、创建) |
| @babel/traverse | 工具类,用来遍历AST树 |
#5.5.3 Babel的插件实战
#实现一个Optional Chaining
foo?.bar
// --------------- 把上面的转换成下面的
foo==null?void 0: foo.bar
开发babel插件,首先对比2段代码的AST结构,利用astexplorer工具分别拿到json格式的AST,拿到2段转换后的json后,在利用diffchecker网站对比一下前后变换。
行数变化可以忽略,可以直接从结构上看见从哪里开始变化,可以看出是从OptionaMembeExpression变化成了ConditionalExpression;所以我们可以把 OptionaMembeExpression结构替换成ConditionalExpression。
// foo?.bar
// foo==null?void 0: foo.bar
const template = require('@babel/template').default
module.exports=function OptionlChainingPlugin(babel){
return {
name: 'optional-chaining-plugin',
visitor: {
// 通过刚刚的对比,我们知道就是替换OptionaMembeExpression这个表达式
OptionaMembeExpression(path,state){
// path.replaceWith() 替换为新的节点
// path.remove() // 删除当前节点
// path.skip() //跳过子节点`
path.repalceWith(
// 用 @babel/types这个包构造ConditionalExpression节点,但是这个包已经挂载到了bable上了,所以可以直接载babel访问
// conditionalExpression具体参数可以访问babel官网查看,t.conditionalExpression(test, consequent, alternate)
// 可以从对比图中看出,第一个参数test类型是BinaryExpression,是一个二元判断,也需要我们用babel.types构造
babel.types.conditionalExpression(
// 从babel文档中查看BinaryExpression所需要的的参数t.binaryExpression(operator, left, right)
// operator就是 == left(左值)就是foo right(右值)就是null
babel.types.BinaryExpression(
'==',
babel.types.identifier(path.node.object.name),
babel.types.nulLiteral()
),
template.expression('void 0'), //将字符串转换称号ast
babel.types.memberExpression(
babel.types.identifier(path.node.object.name) // 对象名称
babel.types.identifier(path.node.property.name) // 属性名称
)
)
)
}
}
}
}
5.6 深入webpack:设计思想
#5.6.1 Tapable
#Tapable是啥?
Tapable是一个插件框架,也是Webpack的底层依赖,webpack几乎所有的功能都有插件提供,webpack本身创建了许多hook,各个插件注册在自己感兴趣的hook上,有webpack在相应的时机去调用它们,tapable正是提供了这样的hook体系。
const {
SyncHook, // 同步钩子
SyncBailHook, // 同步熔断钩子
SyncWaterfallHook, // 同步流水钩子
SyncLoopHook, // 同步循环钩子
AsyncParalleHook, // 异步并发钩子
AsyncParallelBaillHook, // 异步并发熔断钩子
AsyncSeriesHook, // 异步串行钩子
AsyncSeriesBailHook, // 异步串行熔断钩子
AsyncSeriesWaterfallHook // 异步串行流水钩子
} = require('tapable')
Tapable的使用
const { SyncHook } = require('tapable')
// 创建实例
const syncHoook=new SyncHook(["name","age"])
// 注册事件
syncHook.tap("1",(name,age)=>{console.log("1",name,age)})
syncHook.tap("2",(name,age)=>{console.log("1",name,age)})
syncHook.tap("3",(name,age)=>{console.log("1",name,age)})
syncHook.call("Harry Potter",18)
// output:
// 1 Harry Potter 18
// 2 Harry Potter 18
// 3 Harry Potter 18
5.6.2 Webpack工作流程
- 初始化配置
初始化既包括配置的初始化,也包括tapable插件体系的初始化,主要就是实例Compiler这个对象
class Compiler extends Tapable {
constructor (context) {
super()
// 实例一系列tapable hook
this.hooks={
shouldEmit: new SyncBailHook(["compilation"]),
done:new AsyncSeriesHook(['stats']),
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(['compiler']),
emit: new AsyncSeriesHook(["compilation"]),
afterEmit: new AsyncSeriesHook(["compilation"])
}
}
}
- 准备工作(初始化Plugins等)
初始化plugin的过程就是依次调用pluginapply的过程
class SourceMapDevToolPlugin {
// 在我们实例化的`Compiler`对象上注册每个钩子的回调函数
apply(compiler){
compiler
.hooks
.compilation
.tap("SourceMapDevToolPlugin",compilation=>{
compilation
.hooks
.afterOptimizeChunkAssets
.tap(xxx,()=>{context,chunks})
})
}
}
- resolve源文件,构建module
- 生成thunk
- 构建资源
- 最终文件生成
事实上从第三步开始,都有plugin注册hook回调函数的方式在参与
#5.6.3 Webpack的主要概念
-
Entry
- Entry是webpack开始分析依赖的入口
- Webpack从Entry开始,遍历整个项目的依赖
module.exports={
entry:'./path/to/my/entry/files.js'
}
module.exports={
entry:{
app:'./src/app.js',
adminApp:'./src/adminApp.js'
}
enrty 可以有一个,也可以有多个
- Output
Output用来指示Webpack将打包后的bundle文件放在什么位置
const path=require('path');
module.exports={
entry:'./path/to/my/entry/files.js',
output:{
path:path.resolve(__dirname,dist),
fileName:'my-fist-webpack-bundle.js'
}
}
- Loader
- Loader能够让Webpack处理非JS/JSON的文件
- 处理:将一切格式转为JS模块,以便Webpack分析依赖关系和方便我们在浏览器中加载
const path=require('path');
module.exports={
entry:'./path/to/my/entry/files.js',
output:{
path:path.resolve(__dirname,dist),
fileName:'my-fist-webpack-bundle.js'
},
module:{
reules:[
{
test:'/.txt$/',use:'raw-loader'
}
]
}
}
- Plugin
插件负责提供更高级的构建、打包功能
const HtmlWebpackPlugin=require('Html-webpack-plugin')
const path=require('path');
module.exports={
entry:'./path/to/my/entry/files.js',
output:{
path:path.resolve(__dirname,dist),
fileName:'my-fist-webpack-bundle.js'
},
module:{
reules:[
{
test:'/.txt$/',use:'raw-loader'
}
]
},
plugins:[
// HtmlWebpackPlugin 为应用生成一个html文件,并且自动注入所有生成的js bundle,这是loader所做不到的
new HtmlWebpackPlugin({template:'./src/index.html'})
]
}
- Mode (webpack4以后)
指明当前的构建任务所处的环境,让webpack针对特定环境启动一些优化项
module.exports={
mode:'production' // 'node' | 'development' 'production'
}
5.7 深入webpack:高级使用
#5.7.1 基本配置
#entry
- 单入口
module.exports={
entry:'./src/index.js',
}
- 多入口
// 要为每个入口命名
module.exports={
home:'./home.js',
about:'./about.js',
contact:'./contact.js',
}
#output
module.exports={
output:{
// 输出bundle文件名,hash是wepack使用散列算法生成一段字符串,这样每次打包的文件名都不样
// 这样浏览器即使缓存,每次也能加载最新代码
filename:'[name].[hash].bundle.js',
// 输出的 chunk文件名,一般是非entry打包出的文件
chunkFilename:'[id].js'
}
}
资源的加载
我们可以使用loader来加载非js的资源
// css/rest.css
body {
margin:0px;
}
// app.js
import './css/reset.css'
对于加载非js的资源我们都应该使用loader,所有要加载css的资源我们可以选择style-loader、css-loader
css-loader使你可以在别的css中可以使用@import的语法引用别的css
style-loader把js代码中import导入的样式文件代码,以一种特殊的方式打包到jsbundle的结果中,然后在js的运行时,将样式自动插入
页面的style标签中。
module.exports={
entry:path.resolve(__dirname,'src/index.js'),
output:{
filename:path.resolve(__dirname,'dist/'
}
mode:"develoment",
plugins:[....],
module:{
reles:[
{
test:/.css$/,
use:['style-loader','css-loader']
}
]
}
}
需要注意的是,loader的执行顺序是反的,从数组的最后往前执行,如果使用使用sass,需要配置最后面;这样等sass-loader执行完后的结果
在交给css-loader,要不然依赖倒置就会出现错误。
module.exports={
entry:path.resolve(__dirname,'src/index.js'),
output:{
filename:path.resolve(__dirname,'dist/'
}
mode:"develoment",
plugins:[....],
module:{
reles:[
{
test:/.css$/,
use:[{
loader:'style-loader',
},
{
loader:'css-loader',
},
{
loader:'sass-loader',
options:{sourceMap:true}
}],
exclude:'/node_modules/'
}
]
}
}
资源的处理
MiniCssExtractPlugin把css抽离出单独的文件
// loader
{
test: /.scss$/,
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader'
}
// plugin
// 抽取css代码
new MiniCssExtractPlugin({
filename:'[name].css?v=[contenthash]'
})
HTML的处理
- HtmlWebpackPlugin
任何js应用都需要由HTML去承载,我们使用HtmlWebpackPlugin去处理项目中的HTML文件
module.exports={
plugins:[
new HtmlWebpackPlugin({
// 输出的文件名
filename:'index.html'
// 模块文件的路径
template:path.resolve(__dirname,'src/index.html'),
// 配置生成页面的标题
title:'webpack-主页'
})
]
}
#静态资源处理
- 开发中的静态资源
图片、字体、音视频等
{
test: /.(png | jpe?g | gif | svg)$/,
use:[{
loader:'url-loader',
options:{
// 小于8192字节的图片打包成base64图片
limit:8192,
name:'images/[name].[hash:8].[ext]',
publicPath:''
}
}]
}
{
test:/.(woff | woff2 | svg | eot | ttf)$/
use:[
loader:'file-loader',
options :{
limit:8192,
name:'font/[name].[ext]?[hash:8]'
}
]
}
js处理
- babel-loader
不另行指定配置的话,会使用项目的.babelrc.json配置
module:{
reles:[
{
test: /.(js | jsx)$/,
use: 'babel-loader',
include:path.resolve(__dirname,'src')
}
]
}
5.7.2 高级使用
#mode
module.exports={
mode:'development' // none | production | development
}
Mode 用来表示当前的webpack运行环境,本质是在不同的环境下,开启一些内置的优化项
#devServer
- 开发调试
想要在代码发生变化后自动编译代码,有三种方式:
- webpack watch mode
- webpack-dev-server
- webpack-dev-middleware
module.exports={
devServer:{
contentBase:__dirname+'dist',
compress:true,
port:9000
}
}
#HMR(模块热替换)
module.exports={
devServer:{
contentBase:__dirname+'dist',
compress:true,
port:9000,
// 开启HMR
hot:true
}
}
用于在无刷新的情况下,根据文件变动刷新页面的局部状态
#代码分离
代码分离
- 为什么要代码分离?
为了将代码分成多个bundle,并灵活定制加载策略(按需加载、并行加载),从而大大提升应用的加载速度。
如何代码分离?
- 入口起起点:使用entry配置手动地分离代码
- 防止重复:使用SplitChunkPlugin去重和分离chunk
- 动态导入:通过在代码中使用动态加载模块的语法来分离代码
- 多入口构建
module.exports={
mode:'development',
entry:{
index:'./src/index.js',
another:'./src/another-module.js'
},
output:{
path:path.resolve(__dirname,'dist'),
filename:'[name].bundle.js'
}
}
最终结果:
index.bundle.js another.bundle.js 问题:
- 资源可能被重复引入
- 不够灵活
- splitChunks
module.exports={
mode:'development',
entry:{
index:'./src/index.js',
another:'./src/another-module.js'
},
output:{
path:path.resolve(__dirname,'dist'),
filename:'[name].bundle.js'
},
// 在webpack4 中将splitChunks统一到了optimization中
optimization :{
// 查询相关用法,不是插询optimization,而是查询SplitChunksPlugin这个插件
splitChunks:{
chunks:'all'
}
}
}
- 动态导入
- import()
es module提供语言级的方法
- reuire.ensure
在没有import方法之前,webpack提供的方法
// 动态导入是异步的
import(/*webpackChunkName:loaash*/,'lodash').then(({default:_})=>{
})
.catch(err=>{
})
5.8 深入webpack:Loader和Plugin详解
#5.8.1 loader的编写
- Webpack Loader的基本结构
// 同步的Loader
module.exports=input => input + input
// 异步的Loader
module.exports=function(){
const callback=this.async()
callback(null,input+input) //返回值用callback传递出去
}
- loader-utils
loader-utils是编写webpack loader的官方工具库
const loaderUtils=require('loader-utils')
module.exports=function (source) {
// 获取配置
const options = loaderUtils.getOptions(this)
const result=source.replace('word',options.name)
return result
}
- loader中的 “洋葱模型”
style-loader->css-loader->postcss-loader
在loader执行的时候webpack从左到右依次调用pitch方法,然后在从右到左调用loader本身(execute的过程)。
const loaderUtils = require("loader-utils")
module.exports= function (input) {
const { text } = loaderUtils.getOptions(this)
return input + input
}
/*
remainingReg 是loader链中排在当前这个loader后面所有的loader以及资源文件组成的一个链接,这个链接我们可以理解为一个路径
在所有的loader处理完毕后,我们可以在webpack中使用一个特殊的require函数,去require这个路径,从而得到当前loader后所有的loader的处理结果。
precedingReq 是loader链中排在当前这个loader前面所有的loader以及资源文件所组成的链接
input 是一个对象,各个loader把共享的数据挂载这个对象上,如果pitch返回一个值;那么webpack就会跳过余下的loader pitch和execute的过程,
也就是说pitch返回阻断了后续loader的执行
*/
module.exports.pitch=function (remainingReg,precedingReq,input) {
console.log(`
remainingReg request :${remainingReg}
precedingReq request :${precedingReq}
Input: ${JSON.stringify(input,null,2)}
`)
return "pitched"
}
- 调试loader
const fs =require('fs')
const path = require("path")
const { runLoaders }= require('loader-runner') //可以创建一个简单loader调试环境
runLoaders(
{
resource : "./demo.txt",
loaders:[path.resolve(__dirname,"./loaders/demo-loader")],
readResource: fs.readFile.bind(fs)
},
(err,result)=> (err? console.error(err):console.log(result))
)
plugin的编写
loader有loader-runner作为调试工具,webpack的plugin因为需要的上下文信息太多了,所以没有一个模拟的环境,如果我们要开发plugin需要 配置webpack,在真实的环境中开发。
编写pugins我们可以进入webpack网站查看相关开发api和hooks。
- 搭建开发环境
const path = require("path");
const DemoPlugin = require("./plugins/demo-plugin.js")
const PATHS={
lib:path.join(__dirname,"app","shake.js"),
build:path.join(__dirname,"build")
}
module.exports={
entry : {
lib:PATHS.lib
},
output:{
path:PATHS.build,
filename:"[name].js"
},
plugins:[new DemoPlugin()]
}
- Compiler 和 Compilation
webpack plugin的本质就是由apply方法的类,通过apply的方法的类我们可以在运行时取得compiler和Compilation这2个实例; Compiler是编译器的实例(即Webpack),Compilation是每一次编译的过程。
module.exports=class DemoPlugin {
constructor(){
this.options=options
}
apply(compiler){
compiler.plugin("emit",(compilation,cb)=>{
cb()
})
}
}
案例实战
编写一个WebpackPlugin,统计Webpack打包结果中各个文件的大小,并以JSON形式输出统计结果。
const webpackRources = require('webpack-sources')
class WebpackSizePlugin {
constructor (options) {
this.options = options
this.PLUGIN_NAME = 'WebpackSizePlugin'
}
apply (complier) {
const outputOptions = complier.options.output // 拿到output配置,拿到文件最终的输出路径是什么
// 我们插件的目的是统计出打包出来文件的大小,所以我们需要注册到打包结果后的hooks上,由于要输出json,所以要在输出硬盘之前
complier.hooks.emit.tap(
this.PLUGIN_NAME, // 插件的名称
compilation => { // 在这个函数中可以读取和操作本次编译的结果
const assets = compilation.assets // 所有的编译结果都可以通过compilation.assets拿到
const buildSize = {}
const files = Object.keys(assets)
let total = 0
for (let file of files) {
const size = assets[file].size()// 拿到字符数
buildSize[file] = size
total += size
}
console.log('Build Size', buildSize)
console.log('Total Size', total)
buildSize.total = total
// 想要webpack生成一个文件,只需这个文件以键值对的形式加入到assets对象中,那么在打包执行完毕之后,webpack会自动帮我们生成
assets[
outputOptions.publicPath + '/' + (this.options ? this.options.fileName : 'build-size.json')
] = new webpackRources.RawSource(JSON.stringify(buildSize, null, 4))
// assets对象中文件的内容,也就是说assets对象中每一项的值它是一个RawSource对象,而不是一个普通的字符串,上面要输出rawsource对象
}
)
}
}
webpack配置
plugins: [new WebpackSizePlugin({ fileName: 'size.json' })]
#扩展学习
5.9 webpack性能优化
#5.9.1 webpack数据分析
#webpack-bundle-analyzer(文件体积分析)
它能分析打包出的文件有哪些,大小占比如何,模块包含关系,依赖项,文件是否重复,压缩后大小
- webpack.config.js
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')
module.exports={
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'disabled', // 不启动展示打包报告的http服务器
generateStatsFile: true, // 是否生成stats.json文件
})
]
}
- package.json
"scripts": {
"build": "webpack",
"start": "webpack serve",
"dev":"webpack --progress",
"analyzer": "webpack-bundle-analyzer --port 8888 ./dist/stats.json"
}
#speed-measure-webpack-plugin(分析打包速度)
- webpack.config.js
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack5-plugin');
const smw = new SpeedMeasureWebpackPlugin();
module.exports = smw.wrap({
mode: "development",
devtool: 'source-map',
...
});
friendly-errors-webpack-pluginK(美化输出日志)
yarn friendly-errors-webpack-plugin node-notifier -D
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const notifier = require('node-notifier');
module.exports = {
mode: "development",
devtool: 'source-map',
context: process.cwd(),
entry: {
main: "./src/index.js",
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js"
},
plugins:[
new HtmlWebpackPlugin(),
new FriendlyErrorsWebpackPlugin({
onErrors: (severity, errors) => {
const error = errors[0];
notifier.notify({
title: "Webpack编译失败",
message: severity + ': ' + error.name, subtitle: error.file || '',
})
}
})
]
};
5.9.2 编译时间优化
#🍅 1. extensions
- 添加extensions后我们在用
require、import的时候不用添加文件扩展名 - 编译的时候会依次添加扩展名进行匹配
module.exports = {
resolve: {
extensions:[".js"、".jsx"、".json"]
}
}
#🍅 2. alias
配置文件别名可以加快webpack查找模块的速度
const elementUi = path.resolve(__dirname,'node_modules/element-ui/lib/theme-chalk/index.css')
module.exports = {
resolve: {
extensions:[".js"、".jsx"、".json"],
alias: {'element-ui'}
}
}
1
2
3
4
5
6
7\
当我们引入elementUi模块的时候,它会直接引入elementUi,不需要从node_modules文件中按模块规则查找
#🍅 3. modules
指定项目的所有第三方模块都是在项目根目录下的node_modules
const elementUi = path.resolve(__dirname,'node_modules/element-ui/lib/theme-chalk/index.css')
module.exports = {
resolve: {
extensions:[".js"、".jsx"、".json"],
modules: ['node_modules']
}
}
#🍅 4. oneOf
- 每个文件对于rules中的所有规则都会遍历一遍,如果使用oneOf,只要能匹配一个就立即退出
- 在oneOf中不能2个配置处理同一类型文件
module.exports = {
module: {
rules: [{
oneOf:[
{
test: /.js$/,
include: path.resolve(__dirname, "src"),
exclude: /node_modules/,
use: [
{
loader: 'thread-loader',
options: {
workers: 3
}
},
{
loader:'babel-loader',
options: {
cacheDirectory: true
}
}
]
},
{
test: /.css$/,
use: ['cache-loader','logger-loader', 'style-loader', 'css-loader']
}
]
}]
}
}
5. external
如果某个库我们不想让它被webpack打包,想让它用cdn的方法是引入,并且不影响我们在程序中以CMD、AMD方式进行使用
下载插件
yarn add html-webpack-externals-plugin -D
在html文件中引入cdn的文件
<script src="https://cdn.abc.com/vue/2.5.11/vue.min.js"></script>
webpack中的配置
externals: {
vue: 'vue',
},
6. resolveLoader
就是指定loader的resolve,只作用于loader;resolve配置用来影响webpack模块解析规则。解析规则也可以称之为检索,索引规则。配置索引规则能够缩短webpack的解析时间,提升打包速度。
module.exports = {
resolve: {
extensions:[".js"、".jsx"、".json"],
modules: ['node_modules']
},
resolveLoader:{
modules: [path.resolve(__dirname, "loaders"),'node_modules'],
},
}
#🍅 7. noParse
- 用于配置哪些模块的文件内容不需要进行解析
- 不需要解析依赖就是没有依赖的第三方大型类库,可以配置这个字段,以提高整体的构建速度
- 使用noparse进行忽略的模块文件中不能使用import、require等语法
module.exports = {
module: {
noParse: /test.js/, // 正则表达式
}
}
8. thread-loader(多进程)
- 把thread-loader放置在其他 loader 之前
- include 表示哪些目录中的 .js 文件需要进行 babel-loader
- exclude 表示哪些目录中的 .js 文件不要进行 babel-loader
- exclude 的优先级高于 include ,尽量避免 exclude ,更倾向于使用 include
module.exports = {
module: {
rules: [{
oneOf:[
{
test: /.js$/,
include: path.resolve(__dirname, "src"),
exclude: /node_modules/,
use: [
{
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1 // 自己电脑的核心数减1
}
},
{
loader:'babel-loader',
options: {
// babel在转移js非常耗时间,可以将结果缓存起来,下次直接读缓存;默认存放位置是 node_modules/.cache/babel-loader
cacheDirectory: true
}
}
]
},
{
test: /.css$/,
use: ['cache-loader','logger-loader', 'style-loader', 'css-loader']
}
]
}]
}
}
8. cache-loader
- 在一些性能开销较大的loader之前添加cache-loader,可以将结果缓存到磁盘中
- 默认保存在 node_modules/.cache/cache-loader 目录下
module.exports = {
module: {
rules: [{
oneOf:[
{
test: /.css$/,
use: ['cache-loader','logger-loader', 'style-loader', 'css-loader']
}
]
}]
}
}
9. hard-source-webpack-plugin
- HardSourceWebpackPlugin 为模块提供了中间缓存,缓存默认的存放路径是 node_modules/.cache/hard-source
- 配置 hard-source-webpack-plugin 后,首次构建时间并不会有太大的变化,但是从第二次开始, 构建时间大约可以减少80% 左右
- webpack5中已经内置了模块缓存,不需要再使用此插件
yarn add hard-source-webpack-plugin -D
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
plugins: [
new HardSourceWebpackPlugin()
]
}
5.9.3 编译体积优化
#🍅 1. 压缩js、css、HTML和图片
- optimize-css-assets-webpack-plugin是一个优化和压缩CSS资源的插件
- terser-webpack-plugin是一个优化和压缩JS资源的插件
- image-webpack-loader可以帮助我们对图片进行压缩和优化
yarn terser-webpack-plugin optimize-css-assets-webpack-plugin image-webpack-
loader -D
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {,
optimization: {
minimize: true
minimizer: [
new TerserPlugin()
]
},
module:{
rules:[
{
test: /.(png|svg|jpg|gif|jpeg|ico)$/,
use: [
'url-loader',
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive:true,
quality: 65
},
optipng: {
enabled: false
},
pngquant: {
quality: '65-90',
speed: 4
},
gifsicle: {
interlaced: false
},
webp: {
quality: 75,
}
}
}
}]
]
},
plugins:[
new HtmlWebpackPlugin({
template: './src/index.html',
minify: {
collapseWhitespace: true,
removeComments: true
}
})
new OptimizeCssAssetsWebpackPlugin(),
]
}
2. 清除无用的css
purgecss-webpack-plugin单独提取CSS并清除用不到的CSS
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const PurgecssPlugin = require("purgecss-webpack-plugin");
const glob = require("glob");
const PATHS = {
src: path.join(__dirname, "src"),
};
module.exports = {,
optimization: {
minimize: true
minimizer: [
new TerserPlugin()
]
},
module:{
rules:[
{
test: /.css$/,
include: path.resolve(__dirname, "src"),
exclude: /node_modules/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
"css-loader",
}]
]
},
plugins:[
new MiniCssExtractPlugin({
filename: "[name].css"
})
new OptimizeCssAssetsWebpackPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true})
}),
]
}
3. Tree shaking
-
webpack默认支持,可在 production mode下默认开启
-
在package.json 中配置:
- "sideEffects": false 所有的代码都没有副作用(都可以进行 tree shaking)
- 可能会把 css和@babel/polyfill 文件干掉可以设置 "sideEffects":["*.css"]
会把以下情况的代码 Tree shaking
- 没有导入和使用
function func1(){
return 'func1';
}
function func2(){
return 'func2';
}
export {
func1,
func2
}
import {func2} from './functions';
var result2 = func2();
console.log(result2);
- 代码不会被执行,不可到达
if(false){
console.log('false')
}
- 代码执行的结果不会被用到
import {func2} from './functions';
func2();
- 代码中只写不读的变量
var a=1
a= 2
3. Scope Hoisting
- Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快,它又译作 "作用域提升",是在 Webpack3 中新推出的功能。
- scope hoisting的原理是将所有的模块按照引用顺序放在一个函数作用域里,然后适当地重命名一 些变量以防止命名冲突
- 这个功能在mode为 下默认开启,开发环境要用 webpack.optimizeModuleConcatenationPlugin 插件
doc.js
export default 'test';
app.js
import str from './doc.js';
console.log(str)
作用域提升
var str = ('test');
console.log(str);
5.9.4 运行速度优化
- 对于大的Web应用来讲,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些 代码块是在某些特殊的时候才会被用到。
- webpack有一个功能就是将你的代码库分割成chunks语块,当代码运行到需要它们的时候再进行 加载
#🍅 1. 入口点分割
module.exports = {
entry: {
index: "./src/index.js",
login: "./src/login.js"
}
}
-
这种方法的问题
- 如果入口chunks之间包含重复的模块(lodash),那些重复模块都会被引入到各个bundle中
- 不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码
#🍅 2. 懒加载
可以用import()方式去引入模块,当需要的时候在加载某个功能对应代码
const Login = () => import(/* webpackChunkName: "login" */'@/components/Login/Login')
3. prefetch
- 使用预先拉取,你表示该模块可能以后会用到。浏览器会在空闲时间下载该模块
- prefetch的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资 源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源
<link rel="prefetch" as="script" href="test.js">此方法添加头部,浏览器会在空闲时间预先拉取该文件
import(/* webpackChunkName: 'login', webpackPrefetch: true
*/'./login').then(result => {
console.log(result.default);
});
4. 提取公共代码
webpack
module.exports = {
output:{
filename:'[name].js',
chunkFilename:'[name].js'
},
entry: {
index: "./src/index.js",
login: "./src/login.js"
},
optimization: {
splitChunks: {
chunks: 'all', // 分割同步异步的代码
minSize: 0, // 最小体积
minRemainingSize: 0, // 代码分割后的最小保留体积,默认等于minSize
maxSize: 0, // 最大体积
minChunks: 1, // 最小代码快
maxAsyncRequests: 30, // 最大异步请求数
maxInitialRequests: 30, // 最小异步请求数
automaticNameDelimiter: '~', // 名称分离符
enforceSizeThreshold: 50000, //执行拆分的大小阈值,忽略其他限制
// (minRemainingSize、maxAsyncRequests、maxInitialRequests)
cacheGroups: {
defaultVendors: {
test: /[\/]node_modules[\/]/,//控制此缓存组选择哪些模块
priority: -10,//一个模块属于多个缓存组,默认缓存组的优先级是负数,自定义缓存组的优先级更高,默认值为0 //如果当前代码块包含已经主代码块中分离出来的模块,那么它将被重用,而不是生成新的模块。这可能会影响块的结果文件名。
},
default: {
minChunks: 2,
priority: -20
}
}
}
}
plugins: [
new HtmlWebpackPlugin({
template:'./src/index.html',
filename:'page1.html',
chunks:['index']
}),
new HtmlWebpackPlugin({
template:'./src/index.html',
filename:'page2.html',
chunks:['login']
}),
]
}
4. CDN
-
最影响用户体验的是网页首次打开时的加载等待。 导致这个问题的根本是网络传输过程耗时大, CDN的作用就是加速网络传输。
-
CDN 又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近 的服务器获取资源,从而加速资源的获取速度
-
用户使用浏览器第一次访问我们的站点时,该页面引入了各式各样的静态资源,如果我们能做到持 久化缓存的话,可以在 http 响应头加上 Cache-control Expires字段来设置缓存,浏览器可以 将这些资源一一缓存到本地
-
用户在后续访问的时候,如果需要再次请求同样的静态资源,且静态资源没有过期,那么浏览器可以直接走本地缓存而不用再通过网络请求资源
-
缓存配置
- HTML文件不缓存,放在自己的服务器上,关闭自己服务器的缓存,静态资源的URL变成指向 CDN服务器的地址
- 静态的JavaScript、CSS、图片等文件开启CDN和缓存,并且文件名带上HASH值
- 为了并行加载不阻塞,把不同的静态资源分配到不同的CDN服务器上
-
域名限制
- 同一时刻针对同一个域名的资源并行请求是有限制 可以把这些静态资源分散到不同的 CDN 服务上去 多个域名后会增加域名解析时间
- 可以通过在 HTML HEAD 标签中 加入去预解析域名,以降低域名解析带来的延迟