4

第二个问题,我想从这个最简单的 HTML 页面开始。

<!DOCTYPE html>
<html>
<head>
  <title>Test</title>
</head>
<body>
  ...
</body>

当我们想写一些样式的时候,我们通常会引入一个外部的 CSS 文件,就像这样:

<link rel="stylesheet" href="style.css">

有时我们可能会想用一个比如说 Bootstrap 这种的 UI 框架,当然我们也是通过一个 <link> 标签去引入:

<link rel="stylesheet" href="bootstrap.css">

当然了,JavaScript 代码也是类似的,我们可能会有 jQuery、Bootstrap、一些插件以及一些业务代码。

<script src="jquery.js"></script>
<script src="bootstrap.js"></script>
<script src="plugin-A.js"></script>
<script src="plugin-B.js"></script>
<script src="app.js"></script>

当我们在多个 JavaScript 文件之间进行通讯时,我们可能会把一个变量挂到 window 上,变成一个全局的变量。当项目变得越来越复杂,这些全局变量也会变得越来越多,在我刚入职计蒜客的时候,我甚至看到一个页面的全局变量多达 40 多个。当我尝试去维护的时候,我发现这些文件的耦合度非常高。并且由于全局变量非常多,很容易就出现 命名冲突 的情况。

而且,在很多时候,我们多个 JS 文件之间是有依赖关系的,比如说我们图中的 plugin-B。js 如果依赖了 plugin-A.js,当别人想使用 plugin-B.js 但是没有引入 plugin-A.js 的时候,那么 plugin-B.js 就不能正常运行了。所以我们遇到了一个比较繁琐的 文件依赖 问题。

所以,我们第二个问题就是:如何解决命名冲突和依赖混乱问题?

首先,之所以我们会有命名冲突问题,是因为那些 .js 文件都是共享作用域的,而且它们还定义了一些全局的变量。我们要做的,就是要 限制作用域,并且 移除全局变量。还有依赖混乱问题,我们可以通过规定一些特殊的语法,来在代码中声明依赖关系,再开发一个工具来自动化处理文件之间的依赖,就可以解决依赖混乱的问题了。

这种做法我们称为 模块化

我们把每一个 .js 文件都视为一个 模块模块内部有自己的作用域,不会影响到全局。并且,我们 约定一些关键词来进行依赖声明和 API 暴露。而这些约定的关键词就是通过制定一些 规范 去进行规范的。

比较有名模块化规范的是 CMD、AMD、CommonJS 和 ES6 Module,它们都是为了实现在浏览器端模块化开发的目的。前面两个规范分别来自 SeaJS 及 RequireJS,这两个规范现在基本已经很少人用了;CommonJS 由于是被 NodeJS 所采用的,所以很多人用;而 ES6 Module 自然是来自去年正式发布的 ECMAScript 2015 所采用的了,以后会逐渐成为最主要的模块化规范。

因为我们这个系列文章中使用的工具都是基于 NodeJS 写的,而且后面的工具还会用到,所以我们就介绍一下 CommonJS 的语法吧。

比如我们想在一个文件名为 foo.js 的模块中把 { bar: 123 } 这个对象暴露出去,让别人能使用,我们可以用 module.exports 这个关键词:

module.exports = {
  bar: 123
}

比如我们某个模块依赖了 foo.js 这个模块,那么我们可以使用 require 这个关键词来声明我们对 foo.js 的依赖:

require('foo.js') // 返回 { bar: 123 }

CommonJS 规范的语法就这么简单。

了解完规范之后呢,我们还需要一个工具来自动处理它们的这些依赖:

Webpack 可以来帮我们解决这个问题。

它的安装方法很简单,用我们上篇文章中学习到的 NPM 就可以了:执行 npm install -g webpack,这样就可以把 webpack 安装到全局下了。

我们往第一个我们文件名为 foo.js 的模块里填入上面的那段代码:

module.exports = { bar: 123 }

然后我们再来写第二个文件去引用它,比如我们叫它 entry.js,我们在里面填入:

var foo = require('./foo.js')
console.log(foo.bar)

接下来,我们就可以开始用 Webpack 了,我们打开 Terminal,进入到当前目录,然后执行:

webpack entry.js --output-filename build/output.js

这条命令的意思是指定 entry.js 为入口文件,最终打包后的文件路径为 build/output.js

没有意外的话,我们就会看到这样的输出:

Hash: 08ed99b71325392159ff
Version: webpack 1.13.0
Time: 68ms
            Asset     Size  Chunks             Chunk Names
./build/output.js  1.69 kB       0  [emitted]  main
   [0] multi main 40 bytes {0} [built]
   [1] ./entry.js 47 bytes {0} [built]
   [2] ./foo.js 32 bytes {0} [built]

这表示我们打包成功了,并且依照我们的配置生成路径为 ./build/output.js(有兴趣的同学可以打开我们打开刚刚生成的 output.js' 看看,结构并不复杂),而这个 output.js` 是可以直接在浏览器里使用的。

使用 Webpack 配置文件

刚刚那种方法是我们直接在命令行里选择入口文件和输出文件,但大家有没有觉得每次都这么敲命令太麻烦了?显然的,除了这种方法之外,我们还可以通过配置文件来实现。我们创建一个叫 webpack.config.js 的文件,在里面这样写:

module.exports = { // 这里就是我们刚刚说的 CommonJS 的关键词
  entry: './entry.js', // 入口文件
  output: {
    path: './build',
    filename: 'output.js' // 输出文件路径及文件名
  }
}

这样写完之后呢,我们现在只需要执行一下 webpack 就可以完成跟刚才一样的编译了。并且,我们还可以加一个 -w(watch)来监听文件的变化,这样我们之后修改文件之后就不用再手动去执行 webpack 了,它自动就会重新编译。

除了 JavaScript 之外,还有 CSS、图片之类的,怎么办呢?

要实现非 JS 文件的模块化,我们需要使用 Webpack 的 loader。Loader 可以帮我们把一些非 JS 的资源变成可以在 JS 中使用。

比如我们想 require 一个 CSS 资源,那我们就会需要 css-loader,它可以把 CSS 文件变成 JS 的语法,然后我们可以再通过 style-loader 把已经被转换成 JS 语法的 CSS 再插入到 DOM 中,让我们的样式生效。我们来试试:

// entry.js
require('./style.css')

// style.css
p { color: red; }

// webpack.config.js
module.exports = {
  entry: './entry.js', // 入口文件
  output: {
    path: './build',
    filename: 'output.js' // 输出文件路径及文件名
  },
  module: {
    loaders: [
      { test: /\.css$/, loader: 'style!css' } // `!` 是管道,loader 会从后往前依次执行
    ]
  }
}

我们同样执行 webpack,就可以打包完成了,之后我们直接把生成出来的 output.js 放到页面里用就可以了。

组件化与目录结构

很多人习惯性地会按照传统把项目设计成这样:

我们可以看到,比如说这个 Button,它把自己的 JS 抽离了出来变成一个模块,自己的样式和模板也分别抽离了出来变成一个模块。

但是我们想,这种按照传统的目录结构有个很大的问题,那就是每个组件的模板、样式和脚本都分别在不同的目录当中,当我们在创建、修改和删除一个组件的时候需要在不同的目录之间来回切换,这样会很麻烦。于是,很自然地我们就会想把每个组件都放到同一个目录当中去:

左边是原来的目录结构,右边是我们重新设计后的目录结构。我们会发现,当我们把目录结构设计成以组件的形式来分割,而不是以传统的 JS、CSS、HTML 这样去分割的时候,会给我们带来很多好处。

我们把这种将模板、样式和逻辑都抽象出来独立出来的做法称之为 组件化

比如说,我们在开发 button 组件的时候,不再需要分别在几个文件夹之间跳来跳去,去修改它们的模板、样式和逻辑。我们只需要在 button 组件的文件夹里修改就好了。

这种设计其实也是跟我们在软件工程中的「关注度分离」原则是非常吻合的,当我们需要对某个组件进行开发的时候,我们只需要关注这个组件的本身,当我们关注的东西越少,我们出错的可能性就越小,代码的内聚性就更好,耦合性也更低。

以我们计蒜客的课程列表页为例,我把组件的划分用框框把它们给突显出来:

比如上面的导航条是一个组件,大的课程列表是一个组件,课程列表里的每个课的面板也是一个组件,列表右上角也引用了一个按钮组件。这么划分完之后呢,我们就可以对划分出来的组件进行分工了。

通常我会在文档中画一个类似这样的图,上面就是各个组件的依赖关系,以及通过不同的颜色来表示不同的开发同学所要去开发的组件。拿到组件分配任务的同学,就可以对自己负责的组件进行开发了。

组件化库

实际上,除了我们手动去做这么一套组件化的工作之外,业界其实已经有比较火的组件化的库和框架了,比如 Facebook 的 React 和我们国内有名的 @尤雨溪 开发的 Vue。

我个人比较喜欢 Vue,因为它可以以非常平滑和优雅的方式融入到一个已经存在的项目里面去,用作者尤雨溪的话来说,那就是小而美。我们在新的项目中几乎完全抛弃 jQuery 而去用 Vue 了,老的项目也在重构中慢慢地引入 Vue。

由于时间的关系,这一块我就不再扩展开去讨论了,大家感觉兴趣的话可以在一会儿结束之后来跟我讨论一下,也可以回去之后自己上网多了解了解。

关于《模块化工具和一些组件化的思想》咱们大概就聊到这里,下一篇将会关于自动化构建工具,敬请期待。

知乎 Live

最后的最后,还是要给我的 Live 打个广告哈!如果你想了解关于前端工程师的自我修养应该是怎样的、如何成为一个优雅的前端工程师,欢迎点击参加:《前端工程师的自我修养》

扩展阅读

这是前端工具链课系列分享第二篇,如果想看第一篇可以点击:《包管理工具》


JasinYip
787 声望60 粉丝