使用 makefile 压缩 JavaScript 代码
一、makefile 初探
1. 什么是 make指令 和 makefile?
make 指令就像它的名字一样 ,用于制作某个文件(make filename),或者根据 makefile target 自动化编译、打包、生成一个文件(可执行文件或压缩文件)。
例如,我们想根据 a.txt 和 b.txt 文件合成 output.txt 文件,可以书写如下 makefile 文件:
output.txt: a.txt b.txt
@# 根据 a.txt b.txt 文件合成 output.txt 文件
@cat a.txt b.txt > output.txt
复制代码
# 制作 output.txt 文件
make output.txt
通常我们用 make 指令构建 c/c++ 项目,当然,我们也可以用 make 指令构建 go 语言项目、java项目及 node.js 项目。
例如,我们用 make 指令编译 hello.cpp 文件
#include <iostream>
using namespace std;
int main(){
cout << "Hello World!";
return 0;
}
makefile 文件:
hellocpp: hello.o
echo "开始编译"
g++ -o hello hello.o
rm -f hello.o
echo "编译结束"
执行 make 指令
# 使用 makefile 执行 hello.cpp
make
# 执行生成的 hello文件
./hello
> Hello World!
没玩过 makefile 的同学肯定以为 makefile 和 shell 脚本很像,没错,makefile 确实可以当做 shell 脚本使用(bushi),接下来我就简单介绍一下 makefile 的基本规则和常见写法。
2、makefile 的结构
makefile 文件是由一系列的规则(rules) 组成的,每条规则的写法如下:
<target> : <prerequisites>
[tab] <commands>
其中,冒号前面的部分表示目标(target),表示执行的动作。目标可以是一个文件名(如上文中的 output.txt),也可以是多个文件名,中间用空格隔开。目标除了是文件名,也可以是操作名,这种在 makefile 中,叫伪目标(phony target),用 .PHONY 声明目标操作。
如果我们想执行 make clean 操作,但是恰好目录中有一个 clean 的文件,那么我们在执行 make clean
的时候,是不会触发 clean 操作的,这个时候我们就要用 .PHONY 声明伪目标.PHONY clean clean: rm -f *.o
我们在 makefile 中一般有一些约定俗成的目标,如:
- make all:编译所有文件
- make install:安装编译好的应用程序
- make clean:清理应用程序,可执行文件,目标文件等。
冒号后面的部分表示前置条件(prerequisites),之间用空格分隔。声明的目标指定了前置条件,如果没有该前置条件匹配的文件,那么就要先生成该前置条件所需要的文件,才能执行目标。
命令(commands)表示如何构建目标文件,每个命令前必须以 tab 开头,可以和 prerequisites 写在一行,不过要用分号做分隔。
前置条件和命令为非必填项,不过其中一个没写另一个就必须要写。
makefile 里主要包含了五个东西:
- 变量的定义
- 显式规则:根据上文的 target-prerequisites-commands 的书写方式,就是显示规则
- 隐晦规则:由于 makefile 具有自动推到的功能,所以隐晦规则可以让我们粗糙的书写makefile。用makefile
- 内置的变量和函数编写的规则就是隐晦规则。
- 文件指示:可以用 include 指令嵌套引入 makefile
- 注释
变量的定义一般都是字符串,和C语言中的宏比较类似,所以我们有的时候也管makefile中的变量称作宏。
和 vue 的差值模板以及 shell、php 的变量一样,我们一般用${VARIABLE} 或 $(VARIABLE) 的方式去使用一个变量。
在 makefile 中,变量有四种声明方式:
VARIABLE = value # 惰性赋值,在执行时扩展,可以递归扩展
VARIABLE := value # 立即赋值
VARIABLE ?= value # 只有在该变量为空时才设置值
VARIABLE += value # 将值追加到变量的尾端
这四种赋值方式的具体区别可以查看 Stack Overflow,因为这四种赋值方式的区别不是本文讨论的重点,所以我就不过多赘述。
除了用户声明的变量外,makefile 中还有内置变量和自动变量。
内置变量分为两类,作为程序名称的变量(如CC),包含程序参数的变量(如CFLAGS)。关于makefile 所有的内置变量可以查看官方文档。
自动变量的值与当前的规则有关。
一般常用的有以下几个:
- $@:当前目标
- $?:比目标更新的前置条件
- $<:第一个前置条件
- $*:与通配符匹配的部分
- $^:所有前置条件
- KaTeX parse error: Expected 'EOF', got '和' at position 5: (@D)和̲(@F):$@ 的目录名和文件名
- KaTeX parse error: Expected 'EOF', got '和' at position 5: (<D)和̲(<F):$< 的目录名和文件名
@指代的是当前的目标文件,如下面这个例子中,@ 指代的是当前的目标文件,如下面这个例子中,@指代的是当前的目标文件,如下面这个例子中,@ 就表示 a.txt 和 b.txt 两个目标文件的文件名;
# 下面两种写法等价
# 写法一
a.txt b.txt:
touch $@
# 写法二
a.txt:
touch a.txt
b.txt:
touch b.txt
<表示第一个前置条件,如下面这个例子中,< 表示第一个前置条件,如下面这个例子中,<表示第一个前置条件,如下面这个例子中,< 就表示 b.txt
# 下面两种写法等价
# 写法一
a.txt: b.txt c.txt
cp $< $@
# 写法二
a.txt: b.txt c.txt
cp b.txt a.txt
除了内置变量和自动变量,makefile 还可以使用内置函数,使用方法和变量一样。官方文档总共列举了总共14种函数,常用的主要有以下几种:
- shell,shell 函数可以执行shell 命令,个人觉得和 shell 里的管道作用很像。例如 dir:=$(shell pwd)
- subst,用于文本替换,用法如下:$(subst from,to,text)
- patsubst,patsubst 函数用于模式匹配的替换,主要用于替换通配符。用法为 $(patsubst pattern,replacement,text) 。例如 $(patsubst %.c,%.o,a.c.c b.c) 可以将 a.c.c和 b.c 替换成 a.c.o 和 b.o。
- wildcard,wildcard 函数可以用空格分格所有匹配此格式的文件列表。例如,$(wildcard .c)可以获取工作目录下的所有的.c*文件列表。
makefile 还有一些其他的语法需要值得注意:
回声:@
正常情况下,make 在执行的过程中会打印每条 command,包括注释,这种现象在 makefile 中叫做回声。如果不想打印回声的话,可以用 @ 操作符来关闭回声。如:
@# 关闭注释
test:
@echo "编译中。。。"
@npm run dev
通配符(wildcard)
make 的通配符和 shell 一样,主要有*、?。
模式匹配
make 的模式匹配主要的操作符是%,允许对文件名进行类似正则的匹配
注释
makefile 的注释和 shell 脚本l 一样,都是 #
循环和判断指令
makefile的循环判断指令和 shell 脚本一样,主要有以下几种:
- ifeq (if eqaul)指令。它包含两个参数,用逗号分隔并用圆括号包围。变量替换在两个参数上执行,然后进行比较。如果两个参数匹配,则遵循 ifeq后面的命令行;否则会被忽略。
- ifneq (if not eqaul)指令。它包含两个参数,用逗号分隔并用圆括号包围。变量替换在两个参数上执行,然后进行比较。如果两个参数不匹配,则遵循ifneq后面的makefile行;否则会被忽略。
- ifdef (if defined)指令。它包含单个参数。如果给定的参数为真,则条件成立。
- ifndef (if not defined)指令。它包含单个参数。如果给定的参数为假,则条件成立。
- else 指令。
- endif 指令结束的语句,每个 if 条件必须以 endif 结尾。
- for-in-do-done,循环
include 指令
include 指令可以引入其他的 makefile 文件。语法如下
include
# 文件名可以包含 shell 格式的文件名匹配。额外的空格是允许的,并且在行的开始处被忽略,但不允许使用制表符 tab(\t)
-include <filename>
# 无论include过程中出现什么错误,都不要报错继续执行。上面那条指令若是找不到include的目标文件,会报错
override 指令
如果想要重新赋值一个变量,则要使用 override 指令。如
override VARIABLE = value
二、使用 make 构建 JavaScript 代码
前面已经简单介绍了下 make 的用法及 makefile 的一些规则,接下来我们讲一讲如何用 make 压缩 JavaScript 代码。
废话不多说,直接上代码:
src_files := $(shell find src -name '*.js')
dist_files := $(patsubst src/%.js, dist/%.min.js, $(src_files))
node_modules: package.json package-lock.json
@npm i uglifyjs
$(dist_files): $(src_files)
@rm -rf dist
@mkdir dist
@npx uglifyjs $^ -cmo $@
all: node_modules $(dist_files)
.PHONY: all
为了方便测试,我们用零宽空格测试文件是否压缩成功。
在根目录下新建 src 目录,并新建 app.js 文件,文件内容为如下代码:
a
虽然在编辑器中,只显示一个字符a,但是该字符串的长度却是 221,文件大小是 601 字节。这是由零宽字符导致的。
那么什么是零宽字符(zero-width space)呢?
用多个字节表示的字符称之为宽字符,我们常见的 Unicode 编码就是宽字符的一种实现,但是宽字符并不一定是 Unicode。
零宽字符,顾名思义,就是宽度为0的字符。零宽字符在浏览器等环境是不可见的,但却是真是存在的,获取字符长度时也占有长度。常用于防止爬虫,数据隐写,也可以用于 DOS 攻击。
以下为浏览器中常见的特殊字符:
零宽空格(zero-width space, ZWSP)用于可能需要换行处。
Unicode: U+200B HTML: ​
零宽不连字 (zero-width non-joiner,ZWNJ)放在电子文本的两个字符之间,抑制本来会发生的连字,而是以这两个字符原本的字形来绘制。
Unicode: U+200C HTML: ‌
零宽连字(zero-width joiner,ZWJ)是一个控制字符,放在某些需要复杂排版语言(如阿拉伯语、印地语)的两个字符之间,使得这两个本不会发生连字的字符产生了连字效果。
Unicode: U+200D HTML: ‍
左至右符号(Left-to-right mark,LRM)是一种控制字符,用于计算机的双向文稿排版中。
Unicode: U+200E HTML: ‎ ‎ 或‎
右至左符号(Right-to-left mark,RLM)是一种控制字符,用于计算机的双向文稿排版中。
Unicode: U+200F HTML: ‏ ‏ 或‏
字节顺序标记(byte-order mark,BOM)常被用来当做标示文件是以UTF-8、UTF-16或UTF-32编码的标记。
Unicode: U+FEFF
知道了零宽字符的含义后,我们来测试 JavaScript 的压缩功能。
在终端输入 make all 指令,打开生成的 dist 目录,查看 app.min.js 文件,发现大小被压缩到 2个字节了。
上面只是简单列举了用 makefile 构建前端项目的例子,在实际开发中,我们可以使用 make -j 开启多线程来提升我们的构建速度,但是在现代的前端构建程序上,如 webpack、rollup,我们也可以使用多进程提升我们的打包速度(如 thread-loader)。所以这里建议大家,在实际的开发场景中,最好用同构的代码来构建我们的项目。
代码晚些时间会长传至 Github。
作者:上沅同学
链接:https://juejin.cn/post/6957594245083430943
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
共同学习,写下你的评论
评论加载中...
作者其他优质文章