第二个问题,我想从这个最简单的 HTML 页面开始。css
<!DOCTYPE html> <html> <head> <title>Test</title> </head> <body> ... </body>
当咱们想写一些样式的时候,咱们一般会引入一个外部的 CSS 文件,就像这样:html
<link rel="stylesheet" href="style.css">
有时咱们可能会想用一个好比说 Bootstrap 这种的 UI 框架,固然咱们也是经过一个 <link>
标签去引入:前端
<link rel="stylesheet" href="bootstrap.css">
固然了,JavaScript 代码也是相似的,咱们可能会有 jQuery、Bootstrap、一些插件以及一些业务代码。jquery
<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 多个。当我尝试去维护的时候,我发现这些文件的耦合度很是高。而且因为全局变量很是多,很容易就出现 命名冲突 的状况。webpack
并且,在不少时候,咱们多个 JS 文件之间是有依赖关系的,好比说咱们图中的 plugin-B。js
若是依赖了 plugin-A.js
,当别人想使用 plugin-B.js
可是没有引入 plugin-A.js
的时候,那么 plugin-B.js
就不能正常运行了。因此咱们遇到了一个比较繁琐的 文件依赖 问题。web
因此,咱们第二个问题就是:如何解决命名冲突和依赖混乱问题?shell
首先,之因此咱们会有命名冲突问题,是由于那些 .js
文件都是共享做用域的,并且它们还定义了一些全局的变量。咱们要作的,就是要 限制做用域,而且 移除全局变量。还有依赖混乱问题,咱们能够经过规定一些特殊的语法,来在代码中声明依赖关系,再开发一个工具来自动化处理文件之间的依赖,就能够解决依赖混乱的问题了。npm
这种作法咱们称为 模块化。bootstrap
咱们把每个 .js
文件都视为一个 模块,模块内部有本身的做用域,不会影响到全局。而且,咱们 约定一些关键词来进行依赖声明和 API 暴露。而这些约定的关键词就是经过制定一些 规范 去进行规范的。segmentfault
比较有名模块化规范的是 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.config.js
的文件,在里面这样写:
module.exports = { // 这里就是咱们刚刚说的 CommonJS 的关键词 entry: './entry.js', // 入口文件 output: { path: './build', filename: 'output.js' // 输出文件路径及文件名 } }
这样写完以后呢,咱们如今只须要执行一下 webpack
就能够完成跟刚才同样的编译了。而且,咱们还能够加一个 -w
(watch)来监听文件的变化,这样咱们以后修改文件以后就不用再手动去执行 webpack
了,它自动就会从新编译。
要实现非 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 打个广告哈!若是你想了解关于前端工程师的自我修养应该是怎样的、如何成为一个优雅的前端工程师,欢迎点击参加:《前端工程师的自我修养》。
这是前端工具链课系列分享第二篇,若是想看第一篇能够点击:《包管理工具》。