Appearance
如何从 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.html
和src
文件夹,并且在 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 就会报错。
解决的方案有四种,选取任意一种即可。
- 全局安装
webpack
:npm install webpack -g
。 - 执行命令换成
./node_modules/.bin/webpack
。 - 在
npm scripts
中添加指令:"build": "webpack" 。 - npx webpack。
在 webpack 打包完成之后,我们可以看到我们的项目中多了一个 dist
文件夹,里面有一个 bundle.js
文件,就是 index.js
和 a.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-loader
和 style-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
对文件进行打包,那么我们还需要增加 webpack
的 babel-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.html
和 bundle_8098bd66.js
,并且在 html 文件中自动将打包生成的 js 文件引入。
总结
就这样我们从 0 到 1 搭建了一个现代前端项目,这整个过程中涉及的前端工程化的相关知识有以下这些。
- JS 运行环境:node
- 包管理工具:npm
- 静态模块打包器:webpack
- JavaScript 编译器:babel
- 本地快速开发工具:dev-server
- ……
有些同学可能会有疑惑:我为什么要从 0 到 1 自己去搭建项目呢?我可以使用脚手架自动创建项目,或者还可以将其他项目的代码 copy 过来呀!
首先,我认为作为一名合格的前端工程师,必须要有能不依赖工具进行开发的能力。 工具只是作为辅助以提高我们的开发效率,我们需要了解其底层的基本原理,而不是只会使用某些框架、工具的 API。当然,你可以直接使用脚手架来生成,但是对于自动生成的项目的配置你一定要有足够的了解!如果有定制化的需求,知道怎么修改吗?知道怎么针对你的项目进行优化吗?
其次,自己从 0 到 1 搭建项目可以让我们更加清楚使用某些工具的目的。比如,为什么要使用 loader?在什么情况下需要使用 css-loader
?什么情况下需要使用 style-loader
?css-loader
和style-loader
有什么区别?这几个问题有没有很熟悉?!是不是在面试过程中经常会被问到?根据我面试候选人的经验来看,确实还有一些人只知其然而不知其所以然。
最后,为什么在小册的开头我会选择要你了解如何从 0 到 1 去搭建一个现代化前端项目呢?就是为了让你了解从 0 到 1 自己创建项目这个过程,知道现代化的前端开发并不是和“刀耕火种时代”一样,只用一个记事本就可以写网页了;前端开发越来越走上工程化的道路了,我们需要用工程化的思想和方式来为我们降本提效。 所以,在接下来的章节中,我们就会具体介绍前端工程化的相关内容,敬请期待。
那就让我们来一起探索前端工程相关的内容吧。在学习过程中,如果你有什么不理解地方,或者有好的经验要分享,欢迎你留言,我们一起交流和进步。