为什么一个文件的代码不能超过300行?

14,571 阅读9分钟

大家好,我是前端林叔,掘金小册《如何写出高质量的前端代码》 作者。

先说观点:在进行前端开发时,单个文件的代码行数推荐最大不超过300行,而超过1000行的都可以认为是垃圾代码,需要进行重构。

为什么是300

当然,这不是一个完全精准的数字,你一个页面301行也并不是什么犯天条的大罪,只是一般情况下,300行以下的代码可读性会更好。

起初,这只是林叔根据自己多年的工作经验拍脑袋拍出来的一个数字,据我观察,常规的页面开发,或者说几乎所有的前端页面开发,在进行合理的组件化拆分后,页面基本上都能保持在300行以下,当然,一个文件20行也并没有什么不妥,这里只是说上限。

但是拍脑袋得出的结论是不能让人信服的,于是林叔突发奇想想做个实验,看看这些开源大佬的源码文件都是多少行,于是我开发了一个小脚本。给定一个第三方的源文件所在目录,读取该目录下所有文件的行数信息,然后统计该库下文件的最长行数、最短行数、平均行数、小于500行/300行/200行/100行的文件占比。

脚本实现如下,感兴趣的可以看一下,不感兴趣的可以跳过看统计结果。统计排除了css样式文件以及测试相关文件。

const fs = require('fs');
const path = require('path');

let fileList = [];       //存放文件路径
let fileLengthMap = {};  //存放每个文件的行数信息
let result = {           //存放统计数据
    min: 0,
    max: 0,
    avg: 0,
    lt500: 0,
    lt300: 0,
    lt200: 0,
    lt100: 0
}
//收集所有路径
function collectFiles(sourcePath){
    const isFile = function (filePath){
        const stats = fs.statSync(filePath);
        return stats.isFile()
    }
    const shouldIgnore = function (filePath){
        return filePath.includes("__tests__")
            || filePath.includes("node_modules")
            || filePath.includes("output")
            || filePath.includes("scss")
            || filePath.includes("style")
    }
    const getFilesOfDir = function (filePath){
        return fs.readdirSync(filePath)
            .map(file => path.join(filePath, file));
    }

    //利用while实现树的遍历
    let paths = [sourcePath]
    while (paths.length){
        let fileOrDirPath = paths.shift();
        if(shouldIgnore(fileOrDirPath)){
            continue;
        }
        if(isFile(fileOrDirPath)){
            fileList.push(fileOrDirPath);
        }else{
            paths.push(...getFilesOfDir(fileOrDirPath));
        }
    }

}

//获取每个文件的行数
function readFilesLength(){
    fileList.forEach((filePath) => {
        const data = fs.readFileSync(filePath, 'utf8');
        const lines = data.split('\n').length;
        fileLengthMap[filePath] = lines;
    })
}

function statisticalMin(){
    let min = Infinity;
    Object.keys(fileLengthMap).forEach((key) => {
        if (min > fileLengthMap[key]) {
            min = fileLengthMap[key];
        }
    })
    result.min = min;
}
function statisticalMax() {
    let max = 0;
    Object.keys(fileLengthMap).forEach((key) => {
        if (max < fileLengthMap[key]) {
            max = fileLengthMap[key];
        }
    })
    result.max = max;
}
function statisticalAvg() {
    let sum = 0;
    Object.keys(fileLengthMap).forEach((key) => {
        sum += fileLengthMap[key];
    })
    result.avg = Math.round(sum / Object.keys(fileLengthMap).length);
}
function statisticalLt500() {
    let count = 0;
    Object.keys(fileLengthMap).forEach((key) => {
        if (fileLengthMap[key] < 500) {
            count++;
        }
    })
    result.lt500 = (count / Object.keys(fileLengthMap).length * 100).toFixed(2) + '%';
}
function statisticalLt300() {
    let count = 0;
    Object.keys(fileLengthMap).forEach((key) => {
        if (fileLengthMap[key] < 300) {
            count++;
        }
    })
    result.lt300 = (count / Object.keys(fileLengthMap).length * 100).toFixed(2) + '%';
}
function statisticalLt200() {
    let count = 0;
    Object.keys(fileLengthMap).forEach((key) => {
        if (fileLengthMap[key] < 200) {
            count++;
        }
    })
    result.lt200 = (count / Object.keys(fileLengthMap).length * 100).toFixed(2) + '%';
}
function statisticalLt100() {
    let count = 0;
    Object.keys(fileLengthMap).forEach((key) => {
        if (fileLengthMap[key] < 100) {
            count++;
        }
    })
    result.lt100 =  (count / Object.keys(fileLengthMap).length * 100).toFixed(2) + '%';
}
//统计
function statistics(){
    statisticalMin();
    statisticalMax();
    statisticalAvg();
    statisticalLt500();
    statisticalLt300();
    statisticalLt200();
    statisticalLt100();
}

//打印
function print(){
    console.log(fileList)
    console.log(fileLengthMap)
    console.log('最长行数:', result.max);
    console.log('最短行数:', result.min);
    console.log('平均行数:', result.avg);
    console.log('小于500行的文件占比:', result.lt500);
    console.log('小于300行的文件占比:', result.lt300);
    console.log('小于200行的文件占比:', result.lt200);
    console.log('小于100行的文件占比:', result.lt100);
}

function main(path){
    collectFiles(path);
    readFilesLength();
    statistics();
    print();
}

main(path.resolve(__dirname,'./vue-main/src'))

利用该脚本我对Vue、React、ElementPlus和Ant Design这四个前端最常用的库进行了统计,结果如下:

小于100行占比小于200行占比小于300行占比小于500行占比平均行数最大行数备注
vue60.8%84.5%92.6%98.0%1121000仅1个模板文件编译的为1000行
react78.0%92.0%94.0%98.0%961341仅1个JSX文件编译的为1341行
element-plus73.6%90.9%95.8%98.875950
ant-design86.9%96.7%98.7%99.5%47722

可以看出95%左右的文件行数都不超过300行,98%的都低于500行,而每个库中超过千行以上的文件最多也只有一个,而且还都是最复杂的模板文件编译相关的代码,我们平时写的业务代码复杂度远远小于这些优秀的库,那我们有什么理由写出那么冗长的代码呢?

从这个数据来看,林叔的判断是正确的,代码行数推荐300行以下,最好不超过500行,禁止超过1000行

为什么不要超过300

现在,请你告诉我,你见过最难维护的代码文件是什么样的?它们有什么特点?

没错,那就是,通常来说,难维护的代码会有3个显著特点:耦合严重、可读性差、代码过长,而代码过长是难以维护的最重要的原因,就算耦合严重、可读性差,只要代码行数不多,我们总还能试着去理解它,但一旦再伴随着代码过长,就超过我们大脑(就像计算机的CPU和内存)的处理上限了,直接死机了。

这是由于我们的生理结构决定的,大脑天然就喜欢简单的事物,讨厌复杂的事物,不信咱们做个小测试,试着读一遍然后记住下面的几个字母:

F H U T L P

怎么样,记住了吗?是不是非常简单,那我们再来看下下面的,还是读一遍然后记住:

J O Q S D R P M B C V X

这次记住了吗?这才12个字母而已,而上千行的代码中,包含各种各样的调用关系、数据结构等,为了搞懂一个功能可能还要跳转好几个函数,这么复杂的信息,是不是对大脑的要求有点过高了。

代码行数过大通常是难以维护的最大原因。

怎么不超过300

现在前端组件化编程这么流行,这么方便,我实在找不出还要写出超大文件的理由,我可以"武断"地说,凡是写出大文件的同学,都缺乏结构化思维和分治思维

面向结构编程,而不是面向细节编程

以比较简单的官网开发为例,喜欢面向细节编程的同学,可能得实现是这样的:

<div>
    <div class="header">
        <img src="logo.png"/>
        <h1>网站名称</h1>
        <!--  其他头部代码    -->
    </div>
    <div class="main-content">
        <div class="banner">
            <ul>
                <li><img src="banner1.png"></li>
                <!--   省略n行代码      -->
            </ul>
        </div>
        <div class="about-us">
            <!--   省略n行代码      -->
        </div>
        <!--   省略n行代码      -->
    </div>
</div>

其中省略了N行代码,通常他们写出的页面都非常的长,光Dom可能都有大几百行,再加上JS逻辑以及CSS样式,轻松超过1000行。

现在假如领导让修改"关于我们"的相关代码,我们来看看是怎么做的:首先从上往下阅读代码,在几千行代码中找到"关于我们"部分的DOM,然后再从几千行代码中找到相关的JS逻辑,这个过程中伴随着鼠标的反复上下滚动,眼睛像扫描仪一样一行行扫描,生怕错过了某行代码,这样的代码维护起来无疑是让人痛苦的。

面向结构开发的同学实现大概是这样的:

<div>
    <Header/>
    <main>
        <Banner/>
        <AboutUs/>
        <Services/>
        <ContactUs/>
    </main>
    <Footer/>
</div>

我们首先看到的是页面的结构、骨架,如果领导还是让我们修改"关于我们"的代码,你会怎么做,是不是毫不犹豫地就进入AboutUs组件的实现,无关的信息根本不会干扰到你,而且AboutUs的逻辑都集中在组件内部,也符合高内聚的编程原则。

特别是关于表单的开发,面向细节编程的情况特别严重,也造成表单文件特别容易变成超大文件,比如下面这个图,在一个表单中有十几个表单项,其中有一个选择商品分类的下拉选择框。

form.png

面向细节编程的同学喜欢直接把每个表单项的具体实现,杂糅在表单组件中,大概如下这样:

<template>
    <el-form :model="formData">
        <!--忽略其他代码-->
        <el-form-item label="商品分类" prop="group">
            <el-select v-model="formData.group"
                       @visible-change="$event && getGroupOptions()"
            >
                <el-option v-for="item in groupOptions"
                           :key="item.id"
                           :label="item.label"
                           :value="item.value"
                ></el-option>
            </el-select>
        </el-form-item>
    </el-form>
</template>

<script>
export default {
    data(){
        return {
            formData: {
                //忽略其他代码
                group: ''
            },
            groupOptions:[]
        }
    },
    methods:{
        groupOptions(){
            //获取分类信息,赋给groupOptions
            this.groupOptions = [];
        }
    }
}
</script>

这还只是一个非常简单的表单项,你看看,就增加了这么多细节,如果是比较复杂点的表单项,其代码就更多了,这么多实现细节混合在这里,你能轻易地搞明白每个表单项的实现吗?你能说清楚这个表单组件的主线任务吗?

面向结构编程的同学会把它抽取为表单项组件,这样表单组件中只需要关心表单初始化、校验规则配置、保存逻辑等应该表单组件处理的内容,而不再呈现各种细节,实现了关注点的分离。

<template>
    <el-form :model="formData">
        <!--忽略其他代码-->
        <el-form-item label="商品分类" prop="group">
            <select-group v-model="formData.group" />
        </el-form-item>
    </el-form>
</template>

<script>
export default {
    data(){
        return {
            formData: {
                //忽略其他代码
                group: ''
            }
        }
    }
}
</script>

分而治之,大事化小

在进行复杂功能开发时,应该首先通过结构化思考,将大功能拆分为N个小功能,具体每个小功能怎么实现,先不用关心,在结构搭建完成后,再逐个问题击破。

仍然以前面提到的官网为例,首先把架子搭出来,每个子组件先不要实现,只要用一个简单的占位符占个位就行。

<div>
    <Header/>
    <main>
        <Banner/>
        <AboutUs/>
        <Services/>
        <ContactUs/>
    </main>
    <Footer/>
</div>

每个子组件刚开始先用个Div占位,具体实现先不管。

<template>
    <div>关于我们</div>
</template>
<script>
export default {
    name: 'AboutUs'
}
</script>

架子搭好后,再去细化每个子组件的实现,如果子组件很复杂,利用同样的方式将其拆分,然后逐个实现。相比上来就实现一个超大的功能,这样的实现更加简单可执行,也方便我们看到自己的任务进度。

可以看到,我们实现组件拆分的目的,并不是为了组件的复用(复用也是组件化拆分的一个主要目的),而是为了更好地呈现功能的结构,实现关注点的分离,增强可读性和可维护性,同时通过这种拆分,将复杂的大任务变成可执行的小任务,更容易完成且能看到进度。

总结

前端单个文件代码建议不超过300行,最大上限为500行,严禁超过100行。

应该面向结构编程,而不是面向细节编程,要能看到一个组件的主线任务,而不被其中的实现细节干扰,实现关注点分离。

将大任务拆分为可执行的小任务,先进行占位,后逐个实现。

本文内容源自我的掘金小册 《如何写出高质量的前端代码》