- Published on
借助脚本重构.less文件
- Authors
- Name
- 祝你好运
背景
在升级next.js过程中,遇到需要把已有的.less和.scss文件重命名为.module.scss。而项目比较大,文件比较多,如果手工改,就会非常费时,而且容易出错。所以就想通过脚本来做这些事情。 计划 首先要总结下我们都要做哪些事情,然后再看如何去做:
- 转换.less文件为.scss文件
- 重命名.scss文件为.module.scss文件
- 所有相关导入需要更新为正确的文件名
- 处理过程中发现的别的问题
然后我们再看下如何做:
- 通过查找,已经有工具(例如less2scss)可以做这个事情,而且从初步转换结果来看,基本都是文件重命名,少部分是变量定义和引用,还有@mixin(@mixin 与 @include)。
- 通过脚本根据规律批量重命名即可
- 因为是要全量替换,所以可以在脚本中通过正则抓去被import的文件名,然后替换一下即可
- 边做边完善脚本
实操
转换.less文件为.scss文件 有一点需要注意,我们不是直接操作根目录,因为node_modules也在根目录,这个目录不需要处理。然后第一步的转换,直接用命令对目标目录操作即可完成:
less2scss -s ./client -r & less2scss -s ./pages -r
重命名.scss文件为.module.scss文件 脚本语言我选了Python,因为这种脚本不需要维护,一次性的,所以并不需要多么完善,简单省事儿就好了。Python在字符串和数组的处理上,还是非常优秀的。 先写个伪代码,方便理解:
for file in 根目录:
if file.endswith(".scss"):
rename(file, file + ".module.scss")
所有相关导入需要更新为正确的文件名 这一步操作是做起来比较费时的,因为细节比较多。其中这个正则为了抓去被引入文件的文件名,以及为了一个表达式能覆盖到单双引号,import后面不带变量名等情况,写的比较复杂。所以这里解释下:
import\s*([\S]*\s*from\s*)?['"][\S]*.less['"]
- \s*是为了包含0或多个空格的情况
- [\S]*是被引入的文件的那个变量
- ([\S]\sfrom\s*)?最外层是一个可选,表示有可能没有。
- ['"]表示是单引号,或者双引号
- [a-zA-Z?=]*,里层的这块是为了包含index.less?noCssModule=true这种情况,后面有问好和参数,其实整个工程里面这种情况也就两处。
- [\S]*.less,这里就是xxx.less。
for file in 根目录: # 不仅源代码里面有样式的引用,样式文件里面也有引用
if file.suffix in [".jsx", ".tsx", ".less", ".scss", ".ts"]:
for line in file:
# regular_expression就是Python里面的re
re_expr = import\s*([a-zA-Z]*\s*from\s*)?['"][\S]*.less['"]
if regular_expression.search(re_expr, line):
# 下面两行顺序不能颠倒
line.replace(".scss", ".module.scss")
line.replace(".less", ".module.scss")
# 这里只是简单说明效果,正常代码中,读取和写入,直接分两次
# 第一次只读取文件,并备份,在备份上做修改,然后关闭文件
# 第二次以写模式打开文件,写入上面的备份,再关闭文件
file.write_with_changes()
处理过程中发现的别的问题 第一个问题就是转换完原来的.less文件没有被删掉,这个简单,脚本再完善下删掉即可。 第二个问题是有些样式文件中,变量定义放到文件尾部,这个在less中可行,在scss中不行。而且就一处,所以解决办法就是跑脚本之前,手动修复(脚本只是解决问题,不必把脚本做的尽善尽美)。 第三个问题是转换过程中有一些case覆盖不到:
.bottomGuideVersion2 {
position: fixed;
z-index: 200;
}
.xxx {
.bottomGuideVersion2();
}
这里的.bottomGuideVersion2
,是less的混入(mixin)。既作为类选择器,又可以混入到别的选择器中。这个case原工具中没有完美解决,第一个类选择器没有变,还是.bottomGuideVersion2
。而引入的地方变成了@include bottomGuideVersion2();
,相当于include一个未定义的mixin,其实算是一个bug,不管怎么说,我们的第一目标是先把我们的东西处理好,这个也可以在脚本中总结规律来解决掉。 遍历样式文件,抓取所有已知mixin遍历样式文件,如果遇到@include xxx()
;,就看xxx是否在已知的mixin里面,如果不在,放到to_fix里遍历样式文件,如果遇到.xxx {
,而且xxx在to_fix里面。
- 把这个变成
@mixin xxx {
。 - 同时在大括号结束的地方插入
.xxx {@include xxx();}
把新的样式文件写回文件中。
上面提到的大括号结束的地方,可以通过直接统计{}
的数量,初始count=-1,遇到{
就减一,遇到}
就加一。如果count=0
,说明就到了可以插入的地方。 同时也可以做一些优化,mixin肯定都是先定义,后使用,所以我们可以边抓去mixin,边记录to_fix。而且在修复的时候,可以倒序遍历文件,这样新的插入,不会影响遍历时候的行数,源文件和backup能对应起来。
在跑起来之后,还有一些边边角角的问题,如rgba()函数里面多了个0,部分import没有指定.scss后缀名。这些case都极少,不需要放到脚本中修复,直接在跑脚本之后,手动修复即可。 原始脚本 由于只是一次性的脚本,所以并没有特别精简和完善,可以对比上面的伪代码一起看。
#conding=utf8
import os
import re
import subprocess
def fix_import(dir_name):
g = os.walk(dir_name)
converted = False
for path, dir_list, file_list in g:
for file_name in file_list:
if file_name.endswith(".jsx") or \
file_name.endswith(".tsx") or \
file_name.endswith(".js") or \
file_name.endswith(".less") or \
file_name.endswith(".scss") or \
file_name.endswith(".ts"):
full_name = os.path.join(path, file_name)
new_file = open(full_name, 'r')
file_lines = new_file.readlines()
backup = list(file_lines)
for index, line in enumerate(file_lines):
result1 = re.search(r"import\s*([\S]*\s*from\s*)?['\"][\S]*.less['\"]", line, re.M|re.I)
result2 = re.search(r"import\s*([\S]*\s*from\s*)?['\"][\S]*.less['\"]", line, re.M|re.I)
if result1 or result2:
new_line = line
if ".module.scss" not in new_line and "swiper.scss" not in new_line:
new_line = new_line.replace(".scss", ".module.scss")
new_line = new_line.replace(".less", ".module.scss")
backup[index] = new_line
converted = True
new_file.close()
if converted:
temp_file = open(full_name, 'w+')
temp_file.writelines(backup)
temp_file.close()
print(full_name)
# 单个文件调试
# if converted:
# break
def fix_mixin(dir_name):
g = os.walk(dir_name)
for path, dir_list, file_list in g:
for file_name in file_list:
if file_name.endswith(".scss"):
full_name = os.path.join(path, file_name)
new_file = open(full_name, 'r')
file_lines = new_file.readlines()
backup = list(file_lines)
# mixin.scss已经有一些预定义的mixin
mixin_set = set(['text-overflow', 'vertical-middle', 'background-common', 'component-box', 'flex-center', 'line-clamp'])
to_fix_mixin = set()
has_error = False
for index, line in enumerate(file_lines):
result = re.search(r"@mixin ([a-zA-Z-_]+)", line, re.M|re.I)
if result:
mixin_set.add(result.group(1))
result = re.search(r"@include ([a-zA-Z0-9-_]+)", line, re.M|re.I)
if result and result.group(1) not in mixin_set:
to_fix_mixin.add(result.group(1))
has_error = True
print('at line: :\n' + line)
if has_error:
print('has error: ' + full_name)
# 这里需要倒序,方便做插入
for index, line in reversed(list(enumerate(file_lines))):
for to_fix in to_fix_mixin:
if '.' + to_fix + ' {' in line:
# 修复mixin
backup[index] = line.replace('.' + to_fix + ' {', '@mixin ' + to_fix + ' {')
count = -1
current_index = index + 1
while count != 0:
current_line = backup[current_index]
for ch in current_line:
if '{' == ch:
count -= 1
elif '}' == ch:
count += 1
current_index += 1
current_index -= 1
spaces = 0
for ch in backup[current_index]:
if ch == ' ':
spaces += 1
else:
break
current_index += 1
backup[current_index: current_index] = [' ' * spaces + '.' + to_fix + ' {\n', ' ' * (spaces + 2) + '@include ' + to_fix + ';\n', ' ' * spaces + '}\n']
to_fix_mixin.add(to_fix)
new_file.close()
if has_error:
temp_file = open(full_name, 'w+')
temp_file.writelines(backup)
temp_file.close()
def delete_less_file(dir_name):
g = os.walk(dir_name)
for path, dir_list, file_list in g:
for file_name in file_list:
if file_name.endswith(".less"):
full_name = os.path.join(path, file_name)
os.remove(full_name)
print('删除:', full_name)
def rename_scss_to_module_scss(dir_name):
g = os.walk(dir_name)
for path, dir_list, file_list in g:
for file_name in file_list:
if file_name.endswith(".module.scss"):
pass
elif file_name.endswith(".scss"):
full_name = os.path.join(path, file_name)
new_file_name = file_name.split('.')
new_file_name[-1] = "module.scss"
new_file_name = ".".join(new_file_name)
new_full_name = os.path.join(path, new_file_name)
os.rename(full_name, new_full_name)
print('重命名:', new_full_name)
if __name__ == "__main__":
fix_import(r"/Users/admin/Desktop/code/company/h5-ssr/client")
fix_import(r"/Users/admin/Desktop/code/company/h5-ssr/pages")
fix_mixin(r"/Users/admin/Desktop/code/company/h5-ssr/client")
fix_mixin(r"/Users/admin/Desktop/code/company/h5-ssr/pages")
delete_less_file(r"/Users/admin/Desktop/code/company/h5-ssr/client")
delete_less_file(r"/Users/admin/Desktop/code/company/h5-ssr/pages")
rename_scss_to_module_scss(r"/Users/admin/Desktop/code/company/h5-ssr/client")
rename_scss_to_module_scss(r"/Users/admin/Desktop/code/company/h5-ssr/pages")
总结 本文是提供一个思路,如何用最小的代价来解决问题。