这不到半年的时间,玩了不少东西。有些以为不错的新技术,直接拿到公司的项目里去挖坑。感受进步很大,可是看看工程,啥都有。单看模块管理,从遗留的requirejs,到我过来改用的browserify,以及如今的es6 module,都有,乱糟糟的感受。而后有天老大发现:如今发布,前端的构建时间比后端还长。从新作构建方案已经变成了一个本身想作,又有可能升值加薪的事~~css
demo在github上,自取。html
我很是推崇分治
的开发方式。改了一个页面,对其余页面最好不要产生任何影响。开发环境也能够单独针对某个页面,不须要去编译打包其余页面的东西。这就要求,除了一些基本不会改变的公用js框架,js库,公用样式,以及logo图片之类的东西,其余的代码,每一个页面的代码彻底独立,最好从文件夹层面的分离。这样的话,好比有个页面,若是再也不须要,直接把文件夹删就ok了。前端
demo
的目录结构是这样的node
两个页面,一个index,一个contact。各自须要的全部代码,资源文件,全在本身的目录。公用的东西在lib里。react
因此这里假设的构建方案
是:jquery
多个页面,每一个页面相互独立,若是页面不要了,直接删了文件夹就ok。webpack
开发时,只构建本身的东西,由于若是项目有20,30个页面,我如今只开发index,打包、watch其余页面的代码,会影响个人开发效率。nginx
发布的时候,全量构建.git
构建的文件路径映射,给出map.json
(我命名为assets-map.json)文件,供路径解析用。es6
有使用后端开发框架的同窗,应该都知道这个说法。只要按照必定的约定去写代码,框架会帮你作一个自动的处理。好比以文件名以controller
结尾的,是控制器,而后对应的路由会自动生成等。
很早以前我就在想,能不能前端也有约定大于配置
的构建方案。我以为各大公司确定有相应的方案,可是我没见到。我但愿一套方案,直接拿过去,npm一下,按照相应的约定去写,构建自动完成。
这里托webpack
的福,能比较容易的作出我满意的方案。webpack
以模块为设计出发点,全部资源都当成模块,css,js,图片,模版文件等等。
// webpack is a module bundler.
// This means webpack takes modules with dependencies
// and emits static assets representing those modules.
因此实际上,我须要知道每一个页面的入口文件,就能自动构建每一个页面的全部代码和资源。(这么一说,我好像什么也不用作-_-!)。而后配合gulp,去动态微调一些配置。gulp + webpack
的基本玩法就是 配置一个基础的webpackConfig,gulp的task里,根据须要,动态微调基本的webpackConfig。
首先看代码怎么写。既然分治
了,那么先只看index
文件夹。目录结构说明以下:
index img -- 文件夹。是人都知道这个文件夹干吗 js -- 文件夹。因此 less -- 文件夹。就不侮辱你们的智商 test -- 一些测试,这里偷懒,里面啥也没有 tools -- 开发环境工具 index.entry.js -- 入口文件
规定因此入口文件都是*.entry.js
,这就是惟一的约定(后面构建时,会找出因此这样命名规则的文件)。固然主要是webpack作了太多的工做。
看一下index.entry.js
的代码
import ReactDom from 'react-dom' import IndexComponent from './js/IndexComponent.js' import './less/index.less' ReactDom.render( ( <div> <IndexComponent/> <div className='avatar'/> </div> ), document.getElementById('mount-dom') ) setTimeout(function(){ require.ensure([],function(){ require('./js/async.js') }) },1000)
使用es6
语法,先各类import
引入依赖(注意react
和react-dom
会放到lib
里,后面说)。包括js,less,setTimeout
模拟按需异步加载js文件。其中index.less
里有样式引用img里的图片
//index.less的代码 .avatar{ background:url(../img/touxiang.jpg) no-repeat; height: 100px; width: 100px; background-size: 100%; }
执行完构建后,assets\dist下
,你会看到
//[hash]为文件的hash,这里写成占位符。 index.entry-[hash].js index.entry-[hash].css img/[hash].jpg
在assets\assets-map.json
,有路径的映射。(这里用的file-loader处理图片,实际url-loader更好,不过用法上,通常就加一个limit,这里就不赘述了)
对webpack熟悉的同窗,应该会以为这很普通。相似下面的配置:
entry: {'/index.entry':"./assets/src/index/index.entry.js"}, output: { filename: "[name]-[chunkhash].js", chunkFilename:'[name].js', path: __dirname + "/dist", libraryTarget:'umd', publicPath:'' }, externals:{ 'react': { root: 'React', commonjs2: 'react', commonjs: 'react', amd: 'react' }, 'jquery': { root: 'jQuery', commonjs2: 'jquery', commonjs: 'jquery', amd: 'jquery' } } module: { loaders: [ { test: /[\.jsx|\.js ]$/, exclude: /node_modules/, loader: "babel-loader?stage=0&optional[]=runtime" }, { test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader') }, { test: /\.less$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!less-loader') }, { test: /\.(png|jpg|gif)$/, loader: 'file-loader?name=img/[hash].[ext]' } ] }, devtool:'source-map', plugins: [ new ExtractTextPlugin("[name].css"), new webpack.optimize.UglifyJsPlugin({ mangle: { except: ['$', 'exports', 'require'] } }), assetsPluginInstance ],
都是些经常使用的配置和插件,有几点须要的注意的地方
output的filename
要有[chunkhash]
,使用[hash]
的话,同一次构建,不一样的entry文件,会是同一个hash
.缘由看文档
使用了assets-webpack-plugin
生成文件的路径映射。
externals
把公用的库排除掉。公用库会去生成lib.js
,lib.css
那多个页面,怎么去实现一块儿构建呢。
上面的entry
里配置项里只有一个index.entry
,若是有两个固然就生成两个页面的代码和资源。相似这样
{ '/contact.entry': './assets/src/contact/contact.entry.js', '/index.entry': './assets/src/index/index.entry.js' }
还记得咱们的约定吗(说着有点怪。。),选出来全部的*.entry.js
文件,稍做处理就行了。
var entries = {} var entryFiles = glob.sync('assets/src/**/*.entry.js'); for(var i = 0;i<entryFiles.length;i++){ var filePath = entryFiles[i]; key = filePath.substring(filePath.lastIndexOf(path.sep),filePath.lastIndexOf('.')) entries[key] = path.join(__dirname,filePath); } var config = _.merge({},webpackConfig) config.entry=entries
上面的代码就是生成一个键值对(key-value pair),key
形如 /*.entry
,value是入口文件的路径。生成完了,设置给config.entry
.
lib实际上就是把上面exteranls
里的东西,统一打个包。
看gulpfile.js 里的lib
task,就是把external
设成{}
.
lib.js
的代码
import React from 'react' import jQuery from 'jquery' import ReactDOM from 'react-dom' import './reset.less' window.React = React window.jQuery = jQuery window.$ = jQuery window.ReactDOM = ReactDOM
就是把以前排除掉公共的东西,都import进来,另外加点全局的样式。
为啥不用CommonsChunkPlugin
?由于这些东西很明显是属于lib的,不用每次都去构建不须要构建的代码。
运行 gulp lib
后,dist下,就会生成lib-[hash].js
和lib-[hash].css
====
实际上,基本的构建已经完成了。对照下上面说的4点
1.多个页面,每一个页面相互独立,若是页面不要了,直接删了文件夹就ok。
index 和 contact的全部东西都是独立的。这点没问题。
2.开发时,只构建本身的东西,由于若是项目有20,30个页面,我如今只开发index,打包、watch其余页面的代码,会影响个人开发效率。
开发环境后面说
3.发布的时候,全量构建.
发布包括:发布lib.js,发布全部页面的静态文件。gulp的default
task先执行lib
task,而后本身打包全部页面的资源。
4.构建的文件路径映射,给出
map.json
(我命名为assets-map.json)文件,供路径解析用。
已经有了,在/assets/assets-map.json里。
这里有个细节注意一下,assets-webpack-plugin
这个插件,默认是把json文件覆盖掉的。对于本demo,lib
和其余是分开,lib
先执行,因此默认lib
相关的路径映射会被覆盖。不覆盖有两个条件
设置属性{update:true}
同一个插件实例
代码以下:
var AssetsPlugin = require('assets-webpack-plugin'); var assetsPluginInstance = new AssetsPlugin({filename:'assets/assets-map.json',update: true,prettyPrint: true}) //而后配置里,plugins加入assetsPluginInstance,这样gulp lib task 和 default task里的assetsPluginInstance是同一个对象。
有了这个映射文件,就能够自动生成路径了。
//getStatic.js 能够直接执行node getStatic.js看结果。 //执行gulp后,生成assets/assets-map.json后,执行下面的命令 var fs =require('fs') var path = require('path') var fileContent = fs.readFileSync(path.join(__dirname,'assets/assets-map.json')) var assetsJson = JSON.parse(fileContent); function getStatic(resourcePath){ var lastIndex = resourcePath.lastIndexOf('.') var name = resourcePath.substr(0,lastIndex), suffix = resourcePath.substr(lastIndex+1); if(name in assetsJson){ return assetsJson[name][suffix] }else{ return resourcePath; } } console.log(getStatic('/lib.js')) console.log(getStatic('/index.entry.css'))
以express + jade 为例
app.locals.getStatic = function(path){ if(isProdction){ return getStatic(path) }else{ //开发环境,return localPath.. } }
而后模板里这样使用。
script(src=getStatic('/lib.js'))
说了半天,到如今没有任何能够看的效果。其实最大的效果,都在assets/dist
里。不过为了你们看效果,多写点,可以运行。运行方式:
github上clone下来,而后
npm i gulp node index.js
浏览器打开
http://localhost:3333/contact.html http://localhost:3333/index.html
效果很简单(简直是粗糙),可是构建的不少方面都有涉及了。
看一下assets/dist
里的index.html
和contact.html
,是彻底静态的页面。若是不须要首屏数据,都是经过ajax生成的话,这就是一个彻底静态化的方案。只须要nginx 指向dist
文件夹下,就发布好了。这里为了你们运行方便,就express 去作静态文件服务器,道理是同样的。
构建彻底静态化的东西,难点只有一个,就是路径的问题。找webpack
的插件吧。这里使用:html-webpack-plugin
.它会根据模板,自动在head里插入chunks
的css,在body底部插入chunks
的js。刚开始的时候,我使用模板文件(路径./assets/webpack-tpl.html')去生成。
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>webpack coc</title> <!--lib是全部页面公用的。--> <!--须要自动生成一下--> <link href="/lib.css" rel="stylesheet"> </head> <body> <div id="mount-dom"></div> <script src="/lib.js"></script> </body> </html>
生成后,index.html
和 contact.html
会插入相应模块的js和css。可是lib呢,怎么把hash
加上?我写了个简单的插件。webpack的插件,最简单的,就是一个function
//帮助函数 function getTplContent(libJs,libCss) { var str = ` <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>webpack coc</title> <link href="${libCss}" rel="stylesheet"> </head> <body> <div id="mount-dom"></div> <script src="${libJs}"></script> </body> </html> `; return str } //插件,只在执行lib时插入。 function libPathPlugin(){ this.plugin("done", function(stats) { var stats = stats.toJson() var chunkFiles = stats.chunks[0].files var libJs ='',libCss=''; for(var i in chunkFiles){ var fileName = chunkFiles[i] if(fileName.endsWith('.js')){ libJs = fileName } if(fileName.endsWith('.css')){ libCss = fileName } } globalTplContent = getTplContent(libJs,libCss) }); }
this.plugin 的第一个参数是构建的阶段,有after-emit
,emit
,done
等。第二个就是负责执行构建逻辑的函数了。关键是这个stats
参数,里面有大量丰富的信息,建议你们把它打印处理,好好看看。这里只须要知道最终生成了文件名是什么。stats.toJson().chunks
里有。这里只有lib
一个模块,因此简单处理一下,就能获得html-webpack-plugin
须要的模板内容。
另外,一个HtmlWebpackPlugin,只能生成一个html,咱们有多entry,有多个HtmlWebpackPlugin。相关的配置都有说明,另外能够看文档。
for(var i in entries){ config.plugins.push(new HtmlWebpackPlugin({ filename:(i +'.html').replace('entry.',''),//index.entry => index.html //template:'./assets/webpack-tpl.html' templateContent:globalTplContent inject: true, chunks:[i] //只注入当前的chunk,index.html注入index.entry })) }
光有一个项目的方案还不行,实际上,咱们如今已经有多个相对独立的前端项目.继续分治
.
我的以为,先后分离,大致上有两种方案:
1.模板 + ajax
。
首屏有loading
提醒的状况下,用户体验尚可。并且理论上,甚至能够作到彻底静态化,既html也是静态的。这样开完完成后,nginx直接指到相关目录,就ok了。我写的demo为了简单,就是全静态化的。
2.node作api中间层
最简单的状况就是node作个api代理,而后顺即可以简单的套个首屏页面。固然加这一层会给前端几乎无限的可能性。你能够实现本身的缓存策略,对感兴趣的数据进行统计(由于api转接,因此用户请求的数据以及返回的数据,都能拿到。)等。就是工做量略有上升,另外要肩负node运维的职责。node挂了怎么办;升级怎么保证不间断服务等。
经过上图能看出来,大体的架构是这样的:
前端分红多个小项目,都依赖于api server
=> 每个前端,都使用上面的 gulp+ webpack
的方案,把页面分开。这与分治思想
相契合.对于一个工程,重写一个页面,代价过高;彻底从写某一个项目,通常也能够接受。
这里没有对公用
的东西进行说明,已经被团队的同窗pk了。准备在写一篇,讲讲commonChunk。
另外没有就开发流程进行说明。还有单页的应用怎么办。
都再起一篇吧。不过大致思路,就是本篇的东西,你们能够根据本身的状况,去修改。
第二篇看这里,总结了实践中几个小问题,公共模块的抽取和性能优化等。