一、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
声明目标操作。
我们在 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 所有的内置变量可以查看官方文档。
自动变量的值与当前的规则有关。
一般常用的有以下几个:
$@
:当前目标$?
:比目标更新的前置条件$<
:第一个前置条件$*
:与通配符匹配的部分$^
:所有前置条件$(@D)
和$(@F)
:$@
的目录名和文件名$(<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 <filename>
# 文件名可以包含 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。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!