riskers/blog

教你一步步从零构建webpack开发多页面环境

riskers opened this issue · 20 comments


使用 webpack 已经将近一年了,期间用它构建过4、5个项目,踩过一些坑,现在用自己的理解记录下来。

我现在教你如何一步一步搭建 webpack 开发的多页面项目。本文项目地址在 https://github.com/riskers/generate-pages-tutorial

首先需要安装:

git clone https://github.com/riskers/generate-pages-tutorial

这里使用的是 webpack 1.x,webpack 2 见文末

注意每一步的 webpack.config.jspageage.json

一、基本 JavaScript 模块的处理

cd 1_multi_pages
npm install
npm run build

查看 webpack.config.js 可以其实就是配置多个 entry 而已,可以看到 dist 下生成编译好的文件:

|--- dist
        |--- page1
                |--- main.js
        |--- page2
                |--- main.js

这里的目录层级和 entry 中的模块名(page1/mainpage2/main)对应。

打开 page1.htmlpage2.html 就可以看到我们的js模块生效了。现在进入下一步!

二、CSS 模块的处理

通过上一步,我们已经解决了 JavaScript 模块的问题,而页面中还有 CSS 。webpack 默认是只处理 JavaScript 的,所以我们要引入 css-loaderstyle-loader 来处理 CSS。

cd 2_css
npm install
npm run build

CSS

{
    test: /\.css$/,
    loaders: ['style', 'css']
}

loader 是专门处理某些模块的处理器。webpack 只能处理 js ,为了能够处理 CSS ,就需要 css-loader;而为了能够在页面中插入 CSS ,还需要 style-loader

打开 page1.html 就可以看到 css 生效了。

less

{
    test: /\.less$/,
    loaders: ['style', 'css', 'less']
}

如果使用的是 less ,就需要安装 lessless-loader

打开 page2.html 就可以看到 less 生效了。

sass

{
    test: /\.scss$/,
    loaders: ['style', 'css', 'sass']
}

如果使用的是 sass ,就需要安装 node-sasssass-loader

打开 page3.html 就可以看到 less 生效了。

postcss

module: {
    loaders: [
        {
            test: /\.css$/,
            include: ROOT + '/src/page4',
            loaders: ['style', 'css', 'postcss']
        }
    ]
},
postcss: function() {
    return [autoprefixer]
}

如果使用的是 sass ,就需要安装 postcss-loader,这里是以 autoprefixer 为例。

打开 page4.html 就可以看到 less 生效了。

生成 CSS 文件

以上方法都是用 JS 生成 CSS,但是实际上,我们需要的是 CSS 文件,可以使用 extract-text-webpack-plugin 来解决。

打开 page5.html 可以看到效果

三、reload

上面2节我们已经掌握 JS 模块和 CSS 模块的处理,并且能够让 CSS 独立生成文件了,现在我们觉得每次修改代码然后 build 再刷新浏览器这个过程实在太慢了,而且也没必要每修改一行代码,就生成新文件,这是构建速度慢的主要原因。

webpack-dev-server 是 webpack 自带的一个开发服务器,支持热替换、代理等等功能。

cd 3_reload
npm install
npm run dev

打开 0.0.0.0:8888/page1.html ,你就可以看到页面了。而且无论你修改 main.jsstyle.csstpl/page1.html 都会让浏览器自动刷新。

这里使用了:

  • html-webpack-plugin: 在页面中自动注入 js 和 css
  • html-webpack-harddisk-plugin: 每次修改 pages/tpl 内文件时,会自动在 pages/html 内生成对应的文件
  • raw-loader: 可以 require html 文件,做到每修改一次 tpl 文件,浏览器自动刷新一次页面

还有一点值得注意,因为 reload 功能是开发时才需要的,所以我们在 build 的时候要把这部分剔除,cross-envDefinePlugin 的配合可以做到这点。

  • cross-env 能够不分系统地在全局注入变量,下面这条命令就是将 DEV 注入 ENV 环境变量
cross-env ENV=DEV webpack-dev-server --inline --hot --quiet --progress --content-base pages/html  --host 0.0.0.0 --port 8888
  • DefinePlugin 将 process.env.ENV 这个环境变量注入 ENV
new webpack.DefinePlugin({
    'ENV': JSON.stringify(process.env.ENV)
})
  • main.js 中就可以区分是开发环境还是生产环境了:
if(ENV == 'DEV') {
    require('pages/html/page1.html')    
}

四、ES2015 && babel

如果你要在 webpack 中配置 ES2015 的开发环境,需要 babel 的帮助:

  • babel-core
  • babel-loader
  • babel-preset-es2015
  • babel-preset-stage-0
  • babel-plugin-add-module-exports
  • babel-plugin-transform-runtime
cd 4_es2015
npm install
npm run dev

然后在 webpack.config.js 中:

{
    test: /\.js$/,
    loader: "babel",
    exclude: /node_modules/
}

注意 exclude: /node_modules/ 很重要,否则 babel 可能会把 node_modules 中所有模块都用 babel 编译一遍!

并且,在根目录下新建 .babelrc

{
    presets: [
        "es2015",
        "stage-0"
    ],
    plugins: [
        "transform-runtime",
        "add-module-exports"
    ]   
}

然后我们就可以写我们可爱的 ES2015 了:

import './style.css'
import { log } from '../common/index.js'

五、引入库

cd 5_library
npm install
npm run dev

CommonsChunkPlugin

CommonsChunkPlugin 是 webpack 自带的插件,能够把公有模块提取出来:

plugins: [
    new webpack.optimize.CommonsChunkPlugin('common','common.js') 
]

HtmlWebpackPlugin 中加入 common/index.js 模块,我们就可以看到 common/index.js 模块被提取到了 common.js 中。否则的话,page1/mainpage2/main 中都会打包 common/index.js

externals

实际开发中,我们还会在页面使用 <script> 引入一些常用库,比如 jQuery ,那么我们需要

// 当在 js 中 require jQuery 时,实际上是指向 `window.jQuery`
externals: {
    jQuery: 'window.jQuery'
}

然后我们就可以在 page1/main.js 中使用 jQuery 了:

import $ from 'jQuery'
$('body')
    .append('<p>this is jQuery render</p>')
    .css('color', '#FFF')

ProvidePlugin

当然,对于 jQuery 这种每个页面都会使用到的库来说,每次都要 import $ from 'jQuery' 显得很不优雅。可以这样配置:

plugins: [
    new webpack.ProvidePlugin({
        $: 'jquery'
    })
]

这样就可以像 page2/main.js 中那样了:

$('body')
    .append('<p>this is jQuery render</p>')
    .css('color', '#3f3f3f')

六、代理

经过上面几个步骤,我们基本上已经完成了 webpack 的开发环境搭建,但是 pages 里全是静态页面,而我们后端实际上使用的可能是 PHP、Python 甚至是 Node 渲染的动态页面。

现在我们要解决的问题是把现有的 webpack 开发环境和已有的后端环境结合起来,我们这里使用的是 webpack-dev-serverproxy 功能:

devServer: {
    proxy: {
        '*': {
            target: 'http://localhost:8000'
        }
    }
}
cd 6_proxy
npm install
npm run dev

为了模拟一个后端环境,这里使用 PHP 自带的 server 在 8000 端口开启服务:

php -S 127.0.0.1:8000 -t ./pages/html

然后打开 http://0.0.0.0:8888/page1.php 就可以看到页面被 webpack-dev-server 代理过来了。你可以在 pages/html 下得到正式的已经配置好资源路径的页面。

执行 npm run build 后,你会在 pages/html 下得到相应 CDN 地址的资源路径,CDN 地址可以在 npm scripts 下配置:

"scripts": {
    "build": "cross-env CDN=http://cdn.a.com webpack"
}

七、团队协作

到此为止,一个 webpack 搭建的多页面开发环境已经完成了,还有一些与 webpack 无关的话题要注意一下。

ESLint

ESLint 是代码检查工具,这里不多介绍了。如果你使用的是 es2015 ,记得安装 babel-eslint 就好。

pre-commit

pre-commit 是一个很好用的工具,你可以使用它强制性地让团队成员在 commit 代码前执行任何命令(ESLint、测试等等)

"scripts": {
    "lint": "eslint app/src/ app/stylesheets"
},
"precommit": [ "lint" ]

注意使用时要先安装:

node node_modules/pre-commit/install.js

八、一个脚手架模板

我还建立了项目 https://github.com/riskers/generate-pages ,它包括了最最基本的 webpack 开发多页面骨架,感兴趣的也可以看看。由其是 map.js 和 模板文件的映射,这种思路应该可以帮你少写很多代码。

最重要的是,赶紧动手开始使用 webpack 吧!

希望这份教程帮到了你 -_-

20170215 更新

最近,webpack 2 终于正式发布了,之前大概看过他的 beta 版本,但是没有正式发布之前我始终没有去实践,这两天得空把 https://github.com/riskers/generate-pages-tutorial 上的代码更新到了 v2 版本。总体来说,webpack2 默认支持 module、提供 tree shaking 是2个比较让我在意的新功能,其他的以后再细细研究了。


向我捐助 | 关于我 | 工作机会


挺实用的

这个也不错,只是不完善:
https://github.com/cooking-demo/multiple-pages-vue

应该是postcss-loader 而不是 post-loader 吧?

@jl1014171068 已修改 谢谢

cd 2_css
npm install
npm run build

这一步我这边build失败了,输出如下:

> @ build E:\demo\generate-pages-tutorial\2_css
> webpack

Hash: 7a71525e1fe34bec0080
Version: webpack 1.14.0
Time: 600ms
        Asset     Size  Chunks             Chunk Names
page1/main.js  1.75 kB       0  [emitted]  page1/main
page2/main.js  11.9 kB       1  [emitted]  page2/main
page3/main.js  11.9 kB       2  [emitted]  page3/main
page4/main.js  1.75 kB       3  [emitted]  page4/main
page5/main.js  1.75 kB       4  [emitted]  page5/main
   [0] ./src/page1/main.js 95 bytes {0} [built] [1 error]
   [0] ./src/page2/main.js 96 bytes {1} [built]
   [0] ./src/page3/main.js 96 bytes {2} [built]
   [0] ./src/page5/main.js 95 bytes {4} [built] [1 error]
   [0] ./src/page4/main.js 95 bytes {3} [built] [1 error]
   [1] ./src/page1/style.css 0 bytes [built] [failed]
   [2] ./src/common/index.js 51 bytes {0} {1} {2} {3} {4} [built]
   [9] ./src/page4/style.css 0 bytes [built] [failed]
  [10] ./src/page5/style.css 0 bytes [built] [failed]
    + 6 hidden modules

ERROR in ./src/page1/style.css
Module parse failed: E:\demo\generate-pages-tutorial\2_css\src\page1\style.css Unexpected token (1:5)
You may need an appropriate loader to handle this file type.
SyntaxError: Unexpected token (1:5)
    at Parser.pp$4.raise (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:2221:15)
    at Parser.pp.unexpected (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:603:10)
    at Parser.pp.semicolon (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:581:61)
    at Parser.pp$1.parseExpressionStatement (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:966:10)
    at Parser.pp$1.parseStatement (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:730:24)
    at Parser.pp$1.parseTopLevel (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:638:25)
    at Parser.parse (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:516:17)
    at Object.parse (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:3098:39)
    at Parser.parse (E:\demo\generate-pages-tutorial\2_css\node_modules\webpack\lib\Parser.js:902:15)
    at DependenciesBlock.<anonymous> (E:\demo\generate-pages-tutorial\2_css\node_modules\webpack\lib\NormalModule.js:104:16)
    at DependenciesBlock.onModuleBuild (E:\demo\generate-pages-tutorial\2_css\node_modules\webpack-core\lib\NormalModuleMixin.js:310:10)
    at nextLoader (E:\demo\generate-pages-tutorial\2_css\node_modules\webpack-core\lib\NormalModuleMixin.js:275:25)
    at E:\demo\generate-pages-tutorial\2_css\node_modules\webpack-core\lib\NormalModuleMixin.js:259:5
    at Storage.finished (E:\demo\generate-pages-tutorial\2_css\node_modules\enhanced-resolve\lib\CachedInputFileSystem.js:38:16)
    at E:\demo\generate-pages-tutorial\2_css\node_modules\graceful-fs\graceful-fs.js:78:16
    at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:445:3)
 @ ./src/page1/main.js 1:0-22

ERROR in ./src/page4/style.css
Module parse failed: E:\demo\generate-pages-tutorial\2_css\src\page4\style.css Unexpected token (1:5)
You may need an appropriate loader to handle this file type.
SyntaxError: Unexpected token (1:5)
    at Parser.pp$4.raise (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:2221:15)
    at Parser.pp.unexpected (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:603:10)
    at Parser.pp.semicolon (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:581:61)
    at Parser.pp$1.parseExpressionStatement (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:966:10)
    at Parser.pp$1.parseStatement (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:730:24)
    at Parser.pp$1.parseTopLevel (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:638:25)
    at Parser.parse (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:516:17)
    at Object.parse (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:3098:39)
    at Parser.parse (E:\demo\generate-pages-tutorial\2_css\node_modules\webpack\lib\Parser.js:902:15)
    at DependenciesBlock.<anonymous> (E:\demo\generate-pages-tutorial\2_css\node_modules\webpack\lib\NormalModule.js:104:16)
    at DependenciesBlock.onModuleBuild (E:\demo\generate-pages-tutorial\2_css\node_modules\webpack-core\lib\NormalModuleMixin.js:310:10)
    at nextLoader (E:\demo\generate-pages-tutorial\2_css\node_modules\webpack-core\lib\NormalModuleMixin.js:275:25)
    at E:\demo\generate-pages-tutorial\2_css\node_modules\webpack-core\lib\NormalModuleMixin.js:259:5
    at Storage.finished (E:\demo\generate-pages-tutorial\2_css\node_modules\enhanced-resolve\lib\CachedInputFileSystem.js:38:16)
    at E:\demo\generate-pages-tutorial\2_css\node_modules\graceful-fs\graceful-fs.js:78:16
    at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:445:3)
 @ ./src/page4/main.js 1:0-22

ERROR in ./src/page5/style.css
Module parse failed: E:\demo\generate-pages-tutorial\2_css\src\page5\style.css Unexpected token (1:5)
You may need an appropriate loader to handle this file type.
SyntaxError: Unexpected token (1:5)
    at Parser.pp$4.raise (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:2221:15)
    at Parser.pp.unexpected (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:603:10)
    at Parser.pp.semicolon (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:581:61)
    at Parser.pp$1.parseExpressionStatement (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:966:10)
    at Parser.pp$1.parseStatement (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:730:24)
    at Parser.pp$1.parseTopLevel (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:638:25)
    at Parser.parse (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:516:17)
    at Object.parse (E:\demo\generate-pages-tutorial\2_css\node_modules\acorn\dist\acorn.js:3098:39)
    at Parser.parse (E:\demo\generate-pages-tutorial\2_css\node_modules\webpack\lib\Parser.js:902:15)
    at DependenciesBlock.<anonymous> (E:\demo\generate-pages-tutorial\2_css\node_modules\webpack\lib\NormalModule.js:104:16)
    at DependenciesBlock.onModuleBuild (E:\demo\generate-pages-tutorial\2_css\node_modules\webpack-core\lib\NormalModuleMixin.js:310:10)
    at nextLoader (E:\demo\generate-pages-tutorial\2_css\node_modules\webpack-core\lib\NormalModuleMixin.js:275:25)
    at E:\demo\generate-pages-tutorial\2_css\node_modules\webpack-core\lib\NormalModuleMixin.js:259:5
    at Storage.finished (E:\demo\generate-pages-tutorial\2_css\node_modules\enhanced-resolve\lib\CachedInputFileSystem.js:38:16)
    at E:\demo\generate-pages-tutorial\2_css\node_modules\graceful-fs\graceful-fs.js:78:16
    at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:445:3)
 @ ./src/page5/main.js 1:0-22

之后我尝试了将loader部分改为如下代码:

loaders: [
	{
		test: /\.css$/,
		loaders: ['style', 'css']
	},
	{
		test: /\.less$/,
		loaders: ['style', 'css', 'less']
	},
	{
		test: /\.scss$/,
		loaders: ['style', 'css', 'sass']
	},
]

或者

loaders: [
	{
		test: /\.less$/,
		loaders: ['style', 'css', 'less']
	},
	{
		test: /\.scss$/,
		loaders: ['style', 'css', 'sass']
	},
	{
		test: /\.css$/,
		loaders: ['style', 'css', 'postcss']
	}
]

或者

loaders: [
	{
		test: /\.less$/,
		loaders: ['style', 'css', 'less']
	},
	{
		test: /\.scss$/,
		loaders: ['style', 'css', 'sass']
	},
	{
		test: /\.css$/,
		loader: extractCSS.extract('style', 'css')
	}
]

编译都能通过。
所以,我怀疑是loader的include属性导致的。

麻烦楼主答疑解惑,谢谢

@F3n67u 因为我是 Mac 没办法测试 windows 下的情况, Mac 下是没问题的。

至于 include 属性,它的值就是指定使用这个 loaders 的范围而已,我这里这么写,只是想把 less、sass、postcss 这些情况全部列出来,显得很乱了。实际应用不会有这种情况的。

xvinc commented

{
test: /.css$/,
loaders: ['style', 'css'],
// include: ROOT + '/src/page1/'
include: path.resolve(__dirname, 'src/page1'),
},
{
test: /.less$/,
loaders: ['style', 'css', 'less']
},
{
test: /.scss$/,
loaders: ['style', 'css', 'sass']
},
{
test: /.css$/,
loaders: ['style', 'css', 'postcss'],
//include: ROOT + '/src/page4'
include: path.resolve(__dirname, 'src/page4'),
},
{
test: /.css$/,
loader: extractCSS.extract('style', 'css'),
//include: ROOT + 'src/page5'
include: path.resolve(__dirname, 'src/page5'),
}

xvinc commented

图片打包也加个

@xvinc 好的 后续加上

第一部分项目地址那里多加了个句号。

2_css 的文件,原先也是发生了ERROR in ./src/page1/style.css
我是这样配置的

module: {
		loaders: [
			{
				test: /\.css$/,
				//include: ROOT + '/src/page1',
				include:path.join(__dirname,'src/page1'),
				loaders: ['style', 'css']
			},
			{
				test: /\.less$/,
				loaders: ['style', 'css', 'less']
			},
			{
				test: /\.scss$/,
				loaders: ['style', 'css', 'sass']
			},
			{
				test: /\.css$/,
				// include: ROOT + '/src/page4',
				include:path.join(__dirname,'src/page4'),
				loaders: ['style', 'css', 'postcss']
			},
			{
				test: /\.css$/,
				// include: ROOT + '/src/page5',
				include:path.join(ROOT,'src/page5'),
				loader: extractCSS.extract('style', 'css')
			}
		]
	}

成功运行

        Asset      Size  Chunks             Chunk Names
 page1/main.js   11.8 kB       0  [emitted]  page1/main
 page2/main.js     12 kB       1  [emitted]  page2/main
 page3/main.js     12 kB       2  [emitted]  page3/main
 page4/main.js   12.3 kB       3  [emitted]  page4/main
 page5/main.js   1.71 kB       4  [emitted]  page5/main
page5/main.css  55 bytes       4  [emitted]  page5/main
   [0] ./src/page3/main.js 96 bytes {2} [built]
   [0] ./src/page1/main.js 95 bytes {0} [built]
   [0] ./src/page5/main.js 95 bytes {4} [built]
   [0] ./src/page4/main.js 95 bytes {3} [built]
   [0] ./src/page2/main.js 96 bytes {1} [built]
   [5] ./src/common/index.js 51 bytes {0} {1} {2} {3} {4} [built]
    + 12 hidden modules
Child extract-text-webpack-plugin:
        + 2 hidden modules

你好我测试了webpack2第3个项目reload,服务启动不了,O(∩_∩)O谢谢
image

@547377507 不要在中文目录下吧

楼主 你好 我不是很理解为什么要全局代理到后端的服务,如果我要使用webpack-hot-middleware做热更新怎么办,HtmlWebpackPlugin插件的template只能是静态的,有没有一种插件可以从远端代理获取到,那整个开发体验就很完美了,代理的话只需要接口代理就行了吧 资源文件全部从webpack-hot-middleware生成到内存里的资源池去取
// --------------2017年7月23日更新-------------------------
感谢楼主帖子给我带来的帮助已经解决问题了,不过在原先您的基础上做了优化特此写了一篇博客
传送

先mark住,然后再看

mark

Clivn commented

mark
最近可能做react多页面

如果是传统的多页面开发,不用3大框架,各个js文件基本无关联,如何打包呢

楼上,你直接多个entry就好了

团队 Gulp + WebPack 组合使用,WebPack 处理 JavaScript 模块构建,其余交给 Gulp 负责,各自发挥特长。另外 WebPack entry 自动生成。