/ webpack / NodeJS

在Web前端开发中使用Node和webpack

标题说的很明白,本文我只介绍使用Nodewebpack来实现的一个Web前端的构建方案,而不是一个Node实现的全栈方案。至于后端,其实我个人觉得有很多比Node更好的选择,但这不是本文的重点。

写在前面

Node火了,似乎你不用Node都不好意思说自己是个全栈!但你确定要在你的项目中使用一个前后端都是JS的全栈?任何一种技术都有它值得推崇的地方,但同样也有它适合和不适的领域,所以我个人在项目的技术体系和方案选择上会综合很多因素来选择相对适合的,从不会撕逼于各种技术社区和流派!

Well,你可能要问,我选择Node+webpack做前端的理由是什么呢?好吧,我承认这是个问题,但其实,我也只是在尝试组合了几种方案之后觉得这个还蛮简单的!因为我的需求本就不复杂:

  • 首先,我需要一个前端资源的bundler;
  • 其次,要兼容当下主流的JS模块化规范(CommonJSAMD之类);
  • 最后,就是要有一个方便本地开发调试的服务。

好啦,废话说的有点多,该放码了!

那就开始吧

Node的安装我不想赘述,很容易就可以寻到针对不同系统的安装Guide,我就当你的系统里已经有Node了,至于webpack的版本,当然是2.x!首先,我们来初始化一个项目,你可以手动或者像我一样使用npm init创建一个package.json,内容大概会是这个样子:

{
  "name": "using-webpack",
  "version": "1.0.0",
  "description": "a sample about using webpack",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "license": "MIT"
}

接下来,我们要安装几个依赖:

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

如果你的js要用到lodash、jQuery之类,可以继续安装它们:

$ npm install --save lodash jquery

这些依赖默认都会安装到node_modules目录下,我们看下package.json,多了devDependenciesdependencies(当然你也可以手动编写package.json然后运行npm install命令来安装这些依赖):

{
  ...
  "devDependencies": {
    "webpack": "^2.6.1",
    "webpack-dev-server": "^2.4.5"
  },
  "dependencies": {
    "jquery": "^3.2.1",
    "lodash": "^4.17.4"
  }
}

下面,我们来创建一个static文件夹用来存放所有的前端资源文件(JS、CSS、图片、字体等等)和一个webpack配置文件webpack.config.js

var path = require('path');

var config = {
  context: path.resolve(__dirname, 'static/'),
  entry: {
    'app': path.resolve(__dirname, 'static/js/main.js')
  },
  output: {
    filename: path.posix.join('js', '[name].bundle.js'),
    publicPath: '/'
  }
}

module.exports = config;

我们需要一个简单的HTML文件static/sample.html

<!DOCTYPE html>
<html>

<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" />

    <title>Sample Page</title>

</head>
<body>
    <div class="wrapper">
        <h1>Sample Page</h1>

        <p>
            This is a sample html page.
        </p>
    </div>
    <script type="text/javascript" src="js/app.bundle.js"></script>
</body>
</html>

接下来我们要在static/js/main.js里写点东西(在页面中追加“Hello World”):

(function(){
    var hello = document.createElement('p');
    hello.innerText = 'Hello World';
    document.getElementsByTagName('div').item(0).appendChild(hello);
})();

我们可以用webpack-dev-server来启动一个开发服务,测试我们的代码,不过,在此之前,我们要来完善一下webpack.config.js,增加devServer相关的配置:

...
var config = {
...
  devServer: {
    contentBase: path.resolve(__dirname, 'static/'),
    publicPath: '/'
  }
}
...

然后,我们就可以通过命令行node_modules/.bin/webpack-dev-server --config ./webpack.config.js来启动DevServer,默认地址是http://localhost:8080/。浏览http://localhost:8080/sample.html就可以看到效果了。

我们可以把上面这个命令配置在package.jsonscripts1,例如下面的配置,这样启动DevServer,只要执行npm start就可以了:

{
...
  "scripts": {
    "start": "node_modules/.bin/webpack-dev-server --config ./webpack.config.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
...
}

JS可以加载并工作了,但对于Web前端而言还远远不够,至少我们还需要个样式表!

加载样式

样式表的加载就需要用到webpack的loaders2了,对于CSS的加载,我们要用到两个Loader:style-loadercss-loader如果你的项目用到LESS或者SASS,那你还需要用到less-loader或者sass-loader)。

安装相关依赖

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

webpack.config.js增加相关的配置

var path = require('path');

var config = {
  ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [ 'style-loader', 'css-loader' ]
      }
    ]
  }
}

module.exports = config;

增加CSS文件static/css/app.css,定义一些样式,例如:

body {
    margin: 0;
    padding: 0
}

.wrapper {
    width: 80%;
    margin: 20px auto;
    min-width: 320px;
}

然后在static/js/main.js中要import这个CSS:


import '../css/app.css';

...

npm start并访问http://localhost:8080/sample.html,是的,我们写的样式生效了,但用浏览器的开发工具看一下你就会发现,这些样式是被webpack打包到app.bundle.js然后由JS写到页面中。如果我们要把样式输出到相应的css文件,那么我们还需要做一点工作,这个时候就要用到ExtractTextWebpackPlugin。首先,我们当然要先安装这个插件:

$ npm install --save-dev extract-text-webpack-plugin

然后,配置我们的webpack.config.js(注意css rule中的publicPath: '../',这是根据你的项目来配置的,会影响到css文件中url()函数所引用资源的生成路径):

var path = require('path');
var ExtractTextPlugin = require("extract-text-webpack-plugin");

var config = {
  ...
  module: {
    rules: [{
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: "css-loader",
          publicPath: '../'
        })
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      filename: path.posix.join('css', '[name].bundle.css')
    }),
  ],
  ...
}

module.exports = config;

最后我们要在HTML中引入CSS:

<!DOCTYPE html>
<html>

<head>
    ...
    <link rel="stylesheet" type="text/css" href="css/app.bundle.css" />
    ...
</head>

...

npm start并浏览http://localhost:8080/sample.html,嗯!Nice~

图片资源

接下来,我们该为页面加上几张图来美化一下了,首先,还是先来安装相应的Loaders,在本例中,我就用file-loader了,因为这个比较简单!执行安装命令:npm install --save-dev file-loader,然后修改webpack.config.js

...
var config = {
  ...
  module: {
    rules: [
      ...
      {
        test: /\.(png|gif|jpe?g)$/,
        loader: 'file-loader',
        options: {
          name: 'img/[name].[ext]'
        }
      }
    ]
  },
  ...
}

module.exports = config;

现在,把要用到的图片放到static/img目录下,就可以在CSS或者HTML中引用图片了。实际的开发中可能还会遇到字体、视频等静态资源的加载,这些都可以通过配置相应的rules来实现。

写到这里,我们已经可以开始基本的Web前端开发工作了。但实际的项目中我们似乎才只是迈出第一步而已!

Build

通常项目发布的时候,我们要将前端资源打包输出,在这个过程中,通常用到的javascript和样式表会被compile或者minify。如果是用到less、sass,会被编译成css,或者用到CoffeeScript之类,也要被编译成javascript。

webpack为我们提供了方便的webpack -p3命令。还是之前的webpack.config.js,稍作修改,增加关于输出路径的配置,我们将发布文件输出到dist/static/

...
output: {
    filename: path.posix.join('js', '[name].bundle.js'),
    path: path.resolve(__dirname, 'dist/static/'),
    publicPath: '/'
  },
...

然后执行node_modules/.bin/webpack -p --config ./webpack.config.js,当然,你也可以把这个命令配置到到package.jsonscripts中:

...
"scripts": {
    "build": "node_modules/.bin/webpack -p --config ./webpack.config.js",
    ...
  },
...

这样,我们只需要执行npm run build就可以了。

不过你可能会发现一个问题:css中用url()引用的图片可以正常的被输出到dist,但是html中直接引用的图片就不行。解决这个问题的一种办法是在js中声明,比如import '../img/sample-pic.jpg'或者require('../img/sample-pic.jpg');或者使用html-loader来处理引用图片的html文件,即:npm install --save-dev html-loader然后在js中require('html-loader!../sample.html')

不过直接使用html-loader的一个问题是,html文件内容会被一起打包到js中,这并不是我们想要的结果。我们可以利用file-loader和extract-loader将其单独输出:

安装依赖:

npm install --save-dev extract-loader

配置webpack.config.js

...
var config = {
  ...
  module: {
    rules: [
      ...
      {
        test: /\.html?$/,
        use: [{
            loader: 'file-loader',
            options: {
              name: '[name].[ext]',
            },
          },
          {
            loader: 'extract-loader',
          },
          {
            loader: 'html-loader'
          }
        ]
      }
    ]
  },
  ...
}

module.exports = config;

现在,再次webpack -p,文件已经可以正确的输出到dist了。不过因为使用了require(),所生成的js中依然会有类似e.exports='module.exports = __webpack_public_path__ + "sample.html";'这种代码。如果你像我一样对代码有着严重的洁癖,可以把相关的require()移出,比如像我把他放到webpack.config.js中另外指定了一个叫html的entry:

var path = require('path');
...
var config = {
  context: path.resolve(__dirname, 'static/'),
  entry: {
    'app': [
      path.resolve(__dirname, 'static/js/main.js'),
      path.resolve(__dirname, 'static/css/app.css')
    ],
    'html': path.resolve(__dirname, 'static/sample.html')
  },
  ...
}

module.exports = config;

当然这还是会输出一个html.bundle.js,不过我们完全可以忽略或者删除这个文件!

关于加载第三方库

实际项目中我们通常还会用到一些第三方的库资源,比如lodashjQuery。只要是npm仓库中有的,都可以通过npm install来安装,并在代码中import或者require()来使用,或者通过webpack.config.js配置将这些库单独打成vendor包:

var path = require('path');

var config = {
  context: path.resolve(__dirname, 'static/'),
  entry: {
    'vendor': ['lodash', 'jquery'],
    ...
  },
  ...
}

module.exports = config;

然后在HTML文件中通过script标签引用:

<!DOCTYPE html>
<html>

<head>
    ...
    <script type="text/javascript" src="js/vendor.bundle.js"></script>
    ...

</head>
...

需要注意的是有些类库的使用可能会有一些问题(不过这类问题大多通过webpack或者类库官方的文档就可以找到说明和解决的办法),比如jQuery会出现Uncaught ReferenceError: jQuery is not defined问题,这个问题是由于webpack打包和minify的过程会对js类库中的变量进行混淆,这会导致jQuery库的$和jQuery全局定义丢失,这个问题可以用ProvidePlugin来解决,webpack.config.js配置:

var webpack = require("webpack");
...
var config = {
  ...
  plugins: [
    ...
    new webpack.ProvidePlugin({
      $: "jquery",
      jQuery: "jquery"
    })
    ...
  ],
  ...
}

module.exports = config;

好了,现在你可以放心的在代码中使用$了。我们可以将之前写的那段测试代码改造一下试试效果:

(function(){
    $('.wrapper').append($('<p>Hello World</p>'));
})();

嗯,看起来,没什么问题了!

当我们项目有多个entry points,而且又有重复引用类库的时候,你会发现类库被重复的打包在各个js中!这一点webpack也为我们想到了,就是CommonsChunkPlugin插件,使用也并不复杂,就是在webpack.config.js中增加配置,在本例中可以这样:

var path = require('path');
var webpack = require("webpack");
...

var config = {
  entry: {
    'vendor': ['jquery'],
    'app': [
      path.resolve(__dirname, 'static/js/main.js'),
      path.resolve(__dirname, 'static/css/app.css')
    ]
  },
  ...
  plugins: [
    ...
    new webpack.optimize.CommonsChunkPlugin({
      name: "vendor",
      minChunks: Infinity,
    })
  ],
  ...
}

module.exports = config;

最后

好了,这个sample算是写完了,完整的代码放在Github上,需要的同学可以戳这里。欢迎交流!


参考:

Devy

独立开发者' 全栈' 80前' 在理想与现实之间寻找平衡'

Read More