Skip to content
On this page

如何从 0 到 1 搭建一个现代前端项目?


记得在大学的《网页开发》这门课上,老师对我们说:“网页开发的入门很简单的,用记事本就可以开发!”然后他就开始在记事本上写了几行 HTML 文件,再直接打开,然后我们就在网页上看到 “Hello World” 了!紧接着又加了几行 style 标签,网页上的 “Hello World” 由黑色变成了红色!最后又写了几行 script 脚本文件,浏览器出现了弹出框!

看到这一系列操作,同学们都直呼 “好简单呀!!”,于是就开始了《网页开发》的第一课。这也是我首次开始接触前端开发,与我同学们的感受一样,当时我对于前端开发的第一印象也是:入门简单。

那么数年过去后,在 2021 年,如果我们要用现代前端技术开发一个 Hello World,又需要怎么做呢?下面我们就来一步一步剖析。

第一步:从 0 到 Hello World

这里需要提前说明一下:在创建项目前,我默认屏幕前的各位已经有了一定的前端基础,如果是毫无基础的同学,可能对有些概念需要再辛苦勤劳一些,比如经常打开 Google 或掘金去搜索下相关的概念和用法。当然,这里我也默认你已经安装了 node 环境和 npm。

首先,创建一个以你项目命名的文件夹📂,比如我的就叫“0-1webpack”,创建完之后通过命令行打开当前目录,然后执行以下命令:

npm init
// 或
npm init -y

紧接着,命令行就会有交互提示,让我们输入一些项目的配置,你可以认真输入或是一路回车跳过稍后再填。

package name: (0-1webpack) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /Users/workspace/self/0-1webpack/package.json:
{
  "name": "0-1webpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Is this OK? (yes) 

在 “Is this OK” 之后,你会发现我们的项目多了一个 package.json 配置文件,文件的内容就是刚刚我们通过命令行输入的内容。当然,除了使用 npm init之外,你也可以手动创建该 package.json 文件并添加配置字段。

因为我们要创建一个前端项目,前端就必然离不开 HTML、CSS、JS 三大模块。那么,接下来,我们在项目中创建 index.htmlsrc 文件夹,并且在 src 文件夹中创建 index.js

touch index.html
mkdir src
touch src/index.js
touch src/style.css

至此,我们的目录已经搭建完毕了。然后我们在 index.html中填入基本 HTML5 标签结构,并引入脚本文件和样式文件。在 index.js 文件中简单操作下 DOM,方便直观地看出来该 JS 文件已经生效。同时在 css 中设置 #app 的字体颜色为 red

// src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" type="text/css" href="./src/style.css"/>
  <title>Document</title>
</head>
<body>
  <div id="app" />
  <script src="src/index.js"></script>
</body
</html>

// src/index.js
const app = document.querySelector('#app')
app.innerHTML = 'Hello World'


// src/style.css
#app{
  color: red
}

然后,我们直接双击 index.html,就可以直接在浏览器上看到红色的 Hello World 了!好了,阶段性胜利了✌🏻。

同时,我们打开 Network 可以看到,同时加载了三个文件。那么猜想一下,如果我们再多增加几个文件呢?结论你应该也知道,无论我们有多少个文件,在请求页面的时候,都是需要通过网络请求下载到本地的。

那么你可以想一下我们平时开发的项目,一个组件就有 HTML、JS、CSS 三个文件,复杂一点的项目可能会有几十个、上百个组件。那么可想而知,如果我们就这样直接在浏览器中加载我们的项目的话,Network 下会有很长一串文件列表,用户的使用体验会严重下降。

所以,这个时候就需要用到我们平时习以为常的打包神器——webpack

第二步:从 0 到 localhost:8080

首先我们先要在我们的项目下安装 webpack(关于 webpack 的介绍,也不是本文的重点,你可以直接移步官网 webpack):

npm install webpack

完成了安装之后,我们就可以开始进行打包操作了,我们的目的就是将多个文件打包为一个文件

我们创建一个入口文件 main.js 在其中引入了 index.js 和 a.js,并完善一下 HTML 和 js 中的内容,具体如下所示:

// src/index.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" type="text/css" href="./src/style.css"/>
  <title>Document</title>
</head>

<body>
  <div>
    <div id="app" ></div>
    <div id="app2" ></div>
  </div>
  <script src="src/main.js" type="module"></script>
</body>
</html>


// src/main.js
import './a.js';
import './index.js'

// src/index.js
const app = document.querySelector('#app')
app.innerHTML = 'Hello World'

// src/a.js
const app2 = document.querySelector('#app2')
app2.innerHTML = 'js'

同时我们也需要设置 webpack 的配置,在根目录下创建 webpack.config.js 配置文件,设置入口文件和输出文件名。

// webpack.config.js
module.exports = {
  entry: {
    index: './src/main.js',
  },
  output: {
    filename: 'bundle.js',
  },
}

然后就可以进行打包了,使用 webpack 打包只需要在终端执行。

webpack

那我们执行一下试试。在这里,新同学可能会有报错 command not found: webpack,这是因为直接执行 webpack 的话,PATH 的值还是全局路径,如果在本地全局没有安装 webpack 就会报错

解决的方案有四种,选取任意一种即可。

  • 全局安装 webpacknpm install webpack -g
  • 执行命令换成 ./node_modules/.bin/webpack
  • npm scripts 中添加指令:"build": "webpack" 。
  • npx webpack。

在 webpack 打包完成之后,我们可以看到我们的项目中多了一个 dist 文件夹,里面有一个 bundle.js 文件,就是 index.jsa.js中的内容,那么我们就完成了打包的操作。

慢着,这个时候,我们再直接打开 .html 文件看一下,emmmm,页面白屏并且报错:

根据错误信息大概可以看出来原因在于使用了 File 协议,应该使用提示的“http, data, chrome, chrome-extension, chrome-untrusted, https”这些协议。要解决这个问题,我们可以使用 DevServer 创建一个支持 http 的本地服务

webpack 也提供了 webpack-dev-server ,支持快速开发应用程序。那么我们来使用一下,万年不变的第一步,先安装依赖:

npm install webpack-dev-server --save-dev

然后在配置文件中新增 devServer 配置:

// webpack.config.js

module.exports = {
  // ...
  devServer: {
    static: {
      directory: './',
    },
    port: 8080
  }
}

同时在 package.json 中新增 dev 指令:

// package.json
{
  // ...
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1",
    "build": "webpack",
    "dev": "webpack-dev-server",  },

}

紧接着,再运行 npm run dev,不出意外的话,是会报错缺少 webpack-cli 的:

The CLI moved into a separate package: webpack-cli
Please install 'webpack-cli' in addition to webpack itself to use the CLI
-> When using npm: npm i -D webpack-cli
-> When using yarn: yarn add -D webpack-cli
#... 

根据提示我们还需要再次安装依赖:

npm install webpack-cli

然后再次尝试 npm run dev ,✿✿ヽ(°▽°)ノ✿ 终于成功了!

> 0-1webpack@1.0.0 dev /Users/workspace/self/0-1webpack

> webpack-dev-server
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8080/
<i> [webpack-dev-server] On Your Network (IPv4): http://30.209.164.16:8080/
<i> [webpack-dev-server] On Your Network (IPv6): http://[fe80::1]:8080/
<i> [webpack-dev-server] Content not from webpack is served from './src' directory
<i> [webpack-dev-middleware] wait until bundle finished: /
asset bundle.js 125 KiB [emitted] [minimized] (name: index) 1 related asset

webpack 5.52.0 compiled successfully in 780 ms

可以简单看下 webpack-dev-server 的提示信息,包括服务运行的地址、编译的大小、时间等信息。那么接下来,就可以在 http://localhost:8080/ 看到我们刚刚的项目了。

至此,我们已经完成了将两个 JS 打包为 1 个 JS 文件的功能,也成功走通了开发环境,可以在本地环境进行开发

第三步:处理 CSS

webpack 是运行在 node 环境中的,所以在打包的时候只能处理 JS 之间的依赖。像 .css 这样的文件不是一个 JavaScript 模块,所以我们就必须要配置对应的 loader 进行处理。那么我们就来看下怎么使用 webpack 处理 CSS。

CSS 模块的打包需要使用 css-loaderstyle-loader 这两个 loader,它们可以在 JavaScript 模块中 import CSS 文件。其中,css-loader 可以帮助我们解析 JS 文件中的 CSS,而 style-loader 则是将 CSS 代码以 <style> 标签的形式添加到页面头部。下面我们来看下怎么操作的~

又是万年不变的第一步,先安装依赖:

npm install css-loader style-loader --save-dev

紧接着在配置文件中配置 loader:

// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [  
      {
        test: /.css$/,   // 正则表达式,表示.css后缀的文件
        use: ['style-loader','css-loader']   // 针对css文件使用的loader,注意有先后顺序,数组项越靠后越先执行
      }
    ]
  }
}

这里我们需要改变一下引入的方式,因为我们 webpack 配置的入口文件是 main.js ,所以需要在 main.js 中引入我们的样式文件 style.css

然后再次执行打包操作,就可以看到 CSS 的部分也被打包到 bundle.js 中了。

第四步:处理 HTML

前面我们已经处理了网页开发的三大技术中的两个:JS 和 CSS,那么还剩最后一个:HTML。

你可以回忆一下我们平时的线上代码,是不是在打包后的文件后面都会有一个 hash 值?这是因为我们想最大程度地利用浏览器的缓存能力,那么如果文件内容有更改,我们就直接更换文件后面的 hash 值,这样对浏览器来说就是新文件,不会命中缓存策略加载旧文件。对于内容没有变的文件,hash 也不改变,这样就使用缓存中的文件。

webpack 也给我们提供了几种不同的 hash 配置。在配置文件的 output 字段中,我们设置导出的 filename 可以指定 hash,有三个值可以选择。

  • [hash]:整个项目共用同一个 hash 值,只要项目里有文件更改,整个项目构建的 hash 值都会更改。
  • [chunkhash]:同一个模块共用一个 hash 值,就算将 JS 和 CSS 分离,其 hash 值也是相同的,修改一处,JS 和 CSS 的 hash 值都会变。
  • [contenthash]:单个文件单独的 hash 值,只要文件内容不一样,产生的 hash 值就不一样。

可以看出来,选择 contenthash 更有利于缓存效果,所以我们就选择 contenthash

// webpack.config.js
module.exports = {
  // ...
  output: {
    filename: 'bundle_[contenthash:8].js',
  },
}

再次执行 webpack 打包之后,我们可以看到,在项目 /dist 文件夹下,多出 bundle_9397f40b.js 文件。

那么问题来了,每次的文件内容更新,hash 的值都会变,那我们在 HTML 中引入文件的地方每次都要去改文件名吗?答案是否定的,webpack 有插件可以处理这些问题 —— html-webpack-plugin

万年第一步,安装依赖:

npm install html-webpack-plugin --save-dev

然后在配置文件新增配置:

// webpack.config.js
module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html', // 输出的文件名, 默认为 index.html
      template: './src/index.html', // 需处理的文件, 我们的 index.html
    })
  ]
}

然后重新 npm run dev ,打开控制台看下这个时候的 HTML,好像有哪里不对的样子!

是的,浏览器展示的页面并不是我们的 index.html,这个是 HtmlWebpackPlugin 插件以我们自己写的 index.html 为模板生成的 HTML5文件。但是值得注意的是:插件会自动将编译后的 JS 文件引入。

第五步:处理兼容性问题

你屁颠屁颠地给老大说开发完了,坐等上线,然而 QA 拿着祖传的 iPhone5 来找你,说在这个手机上打不开,白屏。你又一顿排查之后,发现是 iOS 9 不支持 const 关键字。你虽然心里默默抱怨:“现在谁还在用 iPhone5?!” 但是又不得不去解决这个问题。

那怎么来解决呢?相信你肯定听说过大名鼎鼎的 Babel!关于 Bable 详细的知识点,我会在后续的章节中为你介绍,这里先简单地从 API 层面上说明下怎么用。

Babel 主要用于将采用 ECMAScript 2015+ 语法编写的代码转化为向后兼容的 JavaScript 代码,以便能够运行在旧版本的浏览器或其他环境中。

首先我们安装依赖:

npm install --save-dev @babel/core @babel/cli @babel/preset-env

然后在根目录下创建一个名为 babel.config.json 的配置文件:

// babel.config.json
// 上述浏览器列表仅用于示例。请根据你所需要支持的浏览器进行调整。

{
  "presets": [
    [
      "@babel/env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ]
}

我们使用的是 webpack 对文件进行打包,那么我们还需要增加 webpackbabel-loader 对我们的 JS 文件进行处理。

npm install  --save-dev babel-loader

然后在 webpack 的配置文件中新增配置:

// webpack.config.js

module.exports = {
  // ...
  module: {
    rules: [  
      // ...
      {
        test: /.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
            },
          },
        ],
      },
    ]
  }
}

接下来我们再次进行打包,就可以看到之前我们代码里面的 const 都被降级为了 var

收工

经过以上一系列操作后,我们再执行 npm run build ,看下我们能够打包出来什么东西。

通过上图能够看出,打包之后的内容都在 dist 文件夹中,里面有打包后的 index.htmlbundle_8098bd66.js,并且在 html 文件中自动将打包生成的 js 文件引入。

总结

就这样我们从 0 到 1 搭建了一个现代前端项目,这整个过程中涉及的前端工程化的相关知识有以下这些。

  • JS 运行环境:node
  • 包管理工具:npm
  • 静态模块打包器:webpack
  • JavaScript 编译器:babel
  • 本地快速开发工具:dev-server
  • ……

有些同学可能会有疑惑:我为什么要从 0 到 1 自己去搭建项目呢?我可以使用脚手架自动创建项目,或者还可以将其他项目的代码 copy 过来呀!

首先,我认为作为一名合格的前端工程师,必须要有能不依赖工具进行开发的能力。 工具只是作为辅助以提高我们的开发效率,我们需要了解其底层的基本原理,而不是只会使用某些框架、工具的 API。当然,你可以直接使用脚手架来生成,但是对于自动生成的项目的配置你一定要有足够的了解!如果有定制化的需求,知道怎么修改吗?知道怎么针对你的项目进行优化吗?

其次,自己从 0 到 1 搭建项目可以让我们更加清楚使用某些工具的目的。比如,为什么要使用 loader?在什么情况下需要使用 css-loader ?什么情况下需要使用 style-loadercss-loaderstyle-loader 有什么区别?这几个问题有没有很熟悉?!是不是在面试过程中经常会被问到?根据我面试候选人的经验来看,确实还有一些人只知其然而不知其所以然。

最后,为什么在小册的开头我会选择要你了解如何从 0 到 1 去搭建一个现代化前端项目呢?就是为了让你了解从 0 到 1 自己创建项目这个过程,知道现代化的前端开发并不是和“刀耕火种时代”一样,只用一个记事本就可以写网页了;前端开发越来越走上工程化的道路了,我们需要用工程化的思想和方式来为我们降本提效。 所以,在接下来的章节中,我们就会具体介绍前端工程化的相关内容,敬请期待。

那就让我们来一起探索前端工程相关的内容吧。在学习过程中,如果你有什么不理解地方,或者有好的经验要分享,欢迎你留言,我们一起交流和进步。