window.addEventListener('DOMContentLoaded', ()=>{ const t = window.performance.timing; console.log('first pain time[painTime-navigationStart]:', window.painTime - t.navigationStart); console.log('first pain time[domContentLoadedEventStart-navigationStart]:', t.domContentLoadedEventStart - t.navigationStart); }, false);
经过测试:
link css 会阻塞页面渲染,只有等 css 加载完成以后,页面才会继续渲染
src image 图片不会阻塞页面渲染,但是会占用处理线程,当pengding的请求多于最多处理线程时,会影响后面的请求,比如ajax请求,动态发起script请求。图片本身不影响首屏时间和可交换时间。
js 优先级的比图片高, head里面的css优先级比body的script优先级高
遇到 js 会阻塞页面渲染,只有等 js 加载完成以后,页面才会继续渲染,影响首屏时间。
从chrome network看,js 和 image 线程同时执行总共最多 6 个,如果同时有 6 js 个阻塞,后面的 js 请求也会阻塞(pending)。
从chrome network看,当 image 阻塞超过 6 个时, image 后面的js 很大可能会被阻塞加载。
DOMContentLoaded事件本身不会等待CSS文件、图片、iframe加载完成。
当页面中没有script标签,DOMContentLoaded事件不会等待css、image加载完成。
总共两个域名,delay.png 是单独的一个域名,js 是应用本身的域名。Chrome每个域原始规则的最多六个TCP连接,但这里有两个域名,所以可以同时触发多于6个请求。
queueing:从添加到待处理队列到实际开始处理的时间间隔.
上面大红色框的请求是一起发送的,但是由于浏览器http线程池内可用线程数量有限,但这些先排队等着
之前的http请求使用完成,有空线程了再按队列中的顺序发送请求。如果按照Time排序,就很清晰的看到队列处理的层次结构:
Stalled:浏览器得到要发出这个请求的指令,到请求可以发出的等待时间,一般是代理协商、以及等待可复用的TCP连接释放的时间,不包括DNS查询、建立TCP连接等时间等。此外,这段时间将包括浏览器何时等待已建立的连接可用于重用,并遵循Chrome每个域原始规则的最多六个TCP连接。
Request sent 请求第一个字节发出前到最后一个字节发出后的时间,也就是上传时间
Waiting 请求发出后,到收到响应的第一个字节所花费的时间(Time To First Byte)
Content Download 收到响应的第一个字节,到接受完最后一个字节的时间,就是下载时间
Queued at 209.48ms: 页面访问后,经过 209.48ms后加入请求队列
Started at 210.44ms: 页面访问后,经过 210.44ms后开始进行网络请求
Queueing 0.96ms: 在队列中存放了0.96ms,这个值刚好是 210.44ms - 209.48ms = 0.96ms
chrome://net-internals/#events
]]>在 前端渲染模式 和 asset 渲染模式 章节讲到了基于 React 的前端渲染模式,但都依赖 egg-view-react-ssr 插件,那如何基于已有 egg 模板引擎 (egg-view-nunjucks 或 egg-view-ejs) + Webpack 完全自定义前端方案呢?
html-webpack-plugin
插件生成 HTML 文件,并自动注入 JS/CSS 依赖write-file-webpack-plugin
插件把 Webpack HTML 文件写到本地。Webpack 默认是在内存里面,无法直接读取。这里以 egg-view-nunjucks 为例,其它模板引擎类似。
npm install egg-view-nunjucks --save
npm install egg-webpack --save-dev
// ${root}/package.json{ "dependencies": { "egg-webpack": "^4.0.0", "egg-view-nunjucks": "^2.2.0", }}
// ${root}/config/plugin.local.jsexports.nunjucks = { enable: true, package: 'egg-webpack',};// ${root}/config/plugin.jsexports.nunjucks = { enable: true, package: 'egg-view-nunjucks',};
<!DOCTYPE html><html lang='en'><head> <title>{{title}}</title> <meta name='keywords'> <meta name='description'> <meta http-equiv='content-type' content='text/html;charset=utf-8'> <meta name='viewport' content='initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui'> <link rel='shortcut icon' href='/favicon.ico' type='image/x-icon' /> <!-- html-webpack-plugin 自动注入 css --></head><body> <div id='app'></div> <script type="text/javascript"> window.__INITIAL_STATE__ = {{ }}; </script> <!-- html-webpack-plugin 自动注入 js --></body></html>
// ${root}/config/local.jsmodule.exports = app => { const exports = {}; exports.webpack = { webpackConfigList: require('easywebpack-react').getWebpackConfig() }; return exports;}// ${root}/config/default.jsmodule.exports = app => { const exports = {}; exports.view = { defaultViewEngine: 'nunjucks', mapping: { '.tpl': 'nunjucks' }, }; return exports;}
const egg = require('egg');module.exports = class AppController extends egg.Controller { async home(ctx) { await ctx.render('layout.tpl', { title: '自定义渲染' }); }}
该配置基于 easywebpack 配置,如果要用原生 webpack 请参考:/blog/wumyiw
const HtmlWebpackPlugin = require('html-webpack-plugin');const WriteFilePlugin = require('write-file-webpack-plugin');module.exports = { egg: true, target: 'web', entry: { app: 'app/web/page/app/app.js' }, plugins: [ new HtmlWebpackPlugin({ filename: '../app/view/layout.tpl', template: './app/web/view/layout.tpl' }), new WriteFilePlugin({ test: /\.tpl$/ }) ]};
https://github.com/easy-team/egg-react-webpack-boilerplate/tree/feature/green/html
]]>egret build
egret run -a 自动编译,浏览器不能自动刷新
public constructor() { super(); this.once(egret.Event.ADDED_TO_STAGE, this.onAddToStage, this);}private onAddToStage(event:egret.Event) { this.stage.frameRate = 50;}
通过 addEventListener 注册监听事件,可以是系统和自定义事件, 通过 dispatchEventWith 和 dispatchEventWith 触发事件, 另外可以通过 removeEventListener 移除监听事件
this.fireTimer.addEventListener(egret.TimerEvent.TIMER, this.createBullet, this);this.dispatchEventWith("createBullet", false, data);this.addEventListener(egret.Event.ENTER_FRAME, this.gameViewUpdate, this);
常用系统事件
egret.TouchEvent.TOUCH_MOVE
egret.TimerEvent.TIMER
egret.TouchEvent.TOUCH_MOVE
http://developer.egret.com/cn/example/egret2d/index.html#060-interact-drag-drop
// 鼠标点击时,鼠标全局坐标与e的位置差private _distance:egret.Point = new egret.Point(); this.e.addEventListener(egret.TouchEvent.TOUCH_BEGIN, this.mouseDown, this);this.e.addEventListener(egret.TouchEvent.TOUCH_END, this.mouseUp, this);private mouseDown(evt:egret.TouchEvent){ this._touchStatus = true; this._distance.x = evt.stageX - this.e.x; this._distance.y = evt.stageY - this.e.y; this.stage.addEventListener(egret.TouchEvent.TOUCH_MOVE, this.mouseMove, this);}private mouseMove(evt:egret.TouchEvent){ if( this._touchStatus ) { this.e.x = evt.stageX - this._distance.x; this.e.y = evt.stageY - this._distance.y; }}
anchoroffsetX 和 anchoroffsetX 可以用来作为虚拟的中心点或者参考点
this.fire.x = 200;this.fire.y = 200;this.fire.scaleX = 0.7;this.fire.scaleY = 0.7;this.fire.anchorOffsetX = this.fire.width / 2;this.fire.anchorOffsetY = this.fire.height / 2;
监听
ENTER_FRAME
将会按照帧频进行回调
this.addEventListener( egret.Event.ENTER_FRAME, ( evt:egret.Event )=>{ e.rotation += 3;}, this );
http://developer.egret.com/cn/example/egret2d/index.html#110-text-text-flow-2
const html:egret.TextField = new egret.TextField();html.textFlow = new egret.HtmlTextParser().parser(str);this.addChild(html);
{ "modules":[ { "name": "particle", "path": "../particle/libsrc" } ]}
http://developer.egret.com/cn/github/egret-docs/extension/threes/instructions/index.html
下载地址:https://github.com/egret-labs/egret-game-library/tree/master/particle
教程文档:http://developer.egret.com/cn/github/egret-docs/extension/particle/introduction/index.html
粒子编辑器: http://developer.egret.com/cn/github/egret-docs/tools/Feather/manual/index.html
silver.zip
// 同步加载资源,这种方式只能获取已经缓存过的资源,例如之前调用过loadGroup()被预加载的资源// 可以在 resource/default.res.json 配置const res = RES.getRes("red_icon_png");// 异步获取资源,这种方式可以获取配置中含有的所有资源项。如果缓存中存在,// 直接调用回调函数返回,若不存在,就启动网络加载文件并解析后回调。const res = RES.getResAsync(name:string,compFunc:Function,thisObject:any):void
markman
软件获取已有纹理图元素坐标,用于编写 纹理图 json 文件, 需要先安装 AdobeAIR创建默认背景图和启动按钮,同时创建可滚动背景图。飞机不动, 利用负坐标,背景向下移动,背景图循环滚动,循环利用
点击启动按钮启动游戏,开始创建飞机和发射子弹
监听 egret.Event.ENTER_FRAME 事件,更新飞机和子弹位置,通过不停的更改飞机和子弹的 y 坐标实现子弹发射效果, 通过 hitTestPoint 进行我的子弹与敌机, 敌机子弹与我的飞机进行碰撞检测,进行相应的扣血
创建我的飞机和发射子弹,监听创建子弹事件,监听我的飞机 egret.TouchEvent.TOUCH_MOVE 事件,同时调整飞机和子弹坐标
定时创建不同类型的敌机,同时开火,监听创建子弹事件
增加护卫机子弹发射,滚石场景,BOSS 场景,能量,爆炸,音乐等场景
飞机不动, 背景向下移动,背景图循环滚动,循环利用
通过 egret.TimerEvent.TIMER 事件执行重复操作,比如创建飞机,滚石,发射子弹等
敌机,子弹,滚石,道具都需要进行回收(超出屏幕)和重复利用,同时停止发射等事件
通过设置 子弹的 scaleX 和 scaleY 改变子弹的发射角度和方向, 可以使用坐标递增,递减或 egret.Tween.get 动画实现子弹发射效果
游戏结束,清理现场,包括 TIMER,ENTER_FRAME以及自定义事件
Webpack 启动流程是怎么样的?
Webpack 插件是怎么使用的,怎么保证调用顺序?
Webpack 事件机制是怎么样的?
接下来我将通过从 Webpack 启动流程, 事件机制, 插件机制, 热更新等几方面深入的讲述一下构建 Webpack 内部构建流程。
首先我们来看看webpack的 webpack.js入口定义:
function webpack(options, callback) { ...... // 初始化所有plugin, 包括自定义事件, 事件回调定义 if(options.plugins && Array.isArray(options.plugins)) { compiler.apply.apply(compiler, options.plugins); } var compiler = new Compiler(); // 这里比较关键,如果有提供回调函数,直接启动编译,这个是用于发布构建使用, // 构建文件落地磁盘,需要提供callback进入构建流程;当使用 webpack-dev-middlerware // 和 webpack-hot-middleware 时,不需要提供callback函数, 由 触发。 if(callback) { // 启动 webpack 编译 compiler.run(callback); } return compiler;}
以上图片来自冯淼森的博客
AMDPlugin
NodeSourcePlugin
FunctionModulePlugin
NodeSourcePlugin
LoaderTargetPlugin
EvalSourceMapDevToolPlugin
CompatibilityPlugin
HarmonyModulesPlugin
AMDPlugin
CommonJsPlugin
LoaderPlugin
NodeStuffPlugin
RequireJsStuffPlugin
APIPlugin
ConstPlugin
UseStrictPlugin
RequireIncludePlugin
RequireEnsurePlugin
RequireContextPlugin
ImportPlugin
SystemPlugin
EnsureChunkConditionsPlugin
RemoveParentModulesPlugin
RemoveEmptyChunksPlugin
MergeDuplicateChunksPlugin
FlagIncludedChunksPlugin
OccurrenceOrderPlugin
FlagDependencyExportsPlugin
FlagDependencyUsagePlugin
TemplatedPathPlugin
RecordIdsPlugin
WarnCaseSensitiveModulesPlugin
CachePlugin
JsonpTemplatePlugin
上面是列举的几个重要的事件名,通过打日志发现,你还会发现还有很多自定义事件, 更多事件请参考官网Event Hooks。你可以通过 compiler.plugin(‘事件名’, callback) 的方式监听这些事件,并提供回调函数。通过Webpack构建提供的生命周期事件,你可以控制 Webpack 编译流程的每个环节,从而实现对 Webpack 的自定义扩展功能。
// Tapable.prototype.plugin 定义事件, 一个事件可以多个回调函数compiler.plugin = function plugin(name, fn){ if(Array.isArray(name)) { name.forEach(function(name) { this.plugin(name, fn); }, this); return; } if(!this._plugins[name]) this._plugins[name] = [fn]; else this._plugins[name].push(fn);}
// node_modules/webpack/lib/webpack.jsfunction webpack(options, callback) { ...... // 初始化所有plugin, 同时注册自定义事件和定义事件回调 if(options.plugins && Array.isArray(options.plugins)) { // apply 是每个plugin必须实现的方法 compiler.apply.apply(compiler, options.plugins); } ......}
查阅代码 Webpack 插件代码你会发现, 很多插件会在 apply
里面监听关键事件,然后处理相关逻辑
apply(compiler) { compiler.plugin("compilation", (compilation, params) => { ...... }); compiler.plugin("make", (compilation, callback) => { ...... });}
在 node_modules/tapable/lib/Tapable.js
文件中提供很多触发事件的方法(方法命名好多,1,2,3,4这种命名,怀疑是版本兼容时不停增加导致的),下面介绍一下主要的两个。
compiler.applyPlugins("compile", params);
compiler.applyPluginsAsync("before-compile", params, err => {});
applyPluginsAsyncSeries [ Compiler {
_plugins:
{ ‘before-run’: [Array],
‘this-compilation’: [Array],
compilation: [Array],
‘after-resolvers’: [Array],
‘entry-option’: [Array],
make: [Array],
‘after-emit’: [Array],
‘watch-run’: [Array],
run: [Array],
‘after-compile’: [Array] },
初始化 inputFileSystem/outputFileSystem/watchFileSystem
根据 webpack 配置 target 初始化 对应 Webpack plugin, 同时初始化文件查找
ResolverFactory.createResolver
compiler.apply( // jsonp-script, require-ensure, bootstrap 脚本注入 new JsonpTemplatePlugin(options.output), // __webpack_require__ 定义 new FunctionModulePlugin(options.output), new NodeSourcePlugin(options.node), new LoaderTargetPlugin(options.target));
compiler.apply( new NodeTemplatePlugin({ asyncChunkLoading: options.target === "async-node" }), new FunctionModulePlugin(options.output), new NodeTargetPlugin(), new LoaderTargetPlugin("node"));
compiler.apply(new EntryOptionPlugin()); compiler.applyPluginsBailResult("entry-option", options.context, options.entry); compiler.apply( new CompatibilityPlugin(), new HarmonyModulesPlugin(options.module), new AMDPlugin(options.module, options.amd || {}), new CommonJsPlugin(options.module), new LoaderPlugin(), new NodeStuffPlugin(options.node), new RequireJsStuffPlugin(), new APIPlugin(), new ConstPlugin(), new UseStrictPlugin(), new RequireIncludePlugin(), new RequireEnsurePlugin(), new RequireContextPlugin(options.resolve.modules, options.resolve.extensions, options.resolve.mainFiles), new ImportPlugin(options.module), new SystemPlugin(options.module) );
compiler.apply( new EnsureChunkConditionsPlugin(), new RemoveParentModulesPlugin(), new RemoveEmptyChunksPlugin(), new MergeDuplicateChunksPlugin(), new FlagIncludedChunksPlugin(), new OccurrenceOrderPlugin(true), new FlagDependencyExportsPlugin(), new FlagDependencyUsagePlugin() ); if(options.performance) { compiler.apply(new SizeLimitsPlugin(options.performance)); } compiler.apply(new TemplatedPathPlugin()); compiler.apply(new RecordIdsPlugin()); compiler.apply(new WarnCaseSensitiveModulesPlugin()); if(options.cache) { let CachePlugin = require("./CachePlugin"); compiler.apply(new CachePlugin(options.cache)); }
run(callback) { const startTime = Date.now(); const onCompiled = (err, compilation) => { //console.log('---run:onCompiled'); if(err) return callback(err); if(this.applyPluginsBailResult("should-emit", compilation) === false) { this.applyPlugins("done", stats); return callback(null, stats); } this.emitAssets(compilation, err => { if(err) return callback(err); if(compilation.applyPluginsBailResult("need-additional-pass")) { this.applyPlugins("done", stats); this.applyPluginsAsync("additional-pass", err => { if(err) return callback(err); this.compile(onCompiled); }); return; } this.emitRecords(err => { if(err) return callback(err); this.applyPlugins("done", stats); return callback(null, stats); }); }); }; this.applyPluginsAsync("before-run", this, err => { if(err) return callback(err); this.applyPluginsAsync("run", this, err => { if(err) return callback(err); //console.log('---applyPluginsAsync:run'); this.readRecords(err => { if(err) return callback(err); this.compile(onCompiled); }); }); }); }
NormalModuleFactory: /node_modules/webpack/lib/NormalModuleFactory.js
createNormalModuleFactory() { // /node_modules/webpack/lib/NormalModuleFactory.js const normalModuleFactory = new NormalModuleFactory(this.options.context, this.resolvers, this.options.module || {}); this.applyPlugins("normal-module-factory", normalModuleFactory); return normalModuleFactory; } createContextModuleFactory() { const contextModuleFactory = new ContextModuleFactory( this.resolvers, this.inputFileSystem ); this.applyPlugins("context-module-factory", contextModuleFactory); return contextModuleFactory; } newCompilationParams() { const params = { normalModuleFactory: this.createNormalModuleFactory(), contextModuleFactory: this.createContextModuleFactory(), compilationDependencies: [] }; return params; }
compile(callback) { const params = this.newCompilationParams(); this.applyPluginsAsync("before-compile", params, err => { if(err) return callback(err); this.applyPlugins("compile", params); const compilation = this.newCompilation(params); this.applyPluginsParallel("make", compilation, err => { if(err) return callback(err); compilation.finish(); compilation.seal(err => { if(err) return callback(err); this.applyPluginsAsync("after-compile", compilation, err => { if(err) return callback(err); return callback(null, compilation); }); }); }); });}
// Compilation: node_modules/webpack/lib/Compilation.js
Compilation.addEntry(context, entry, name, callback)
function webpack(options)
new WebpackOptionsDefaulter().process(options);
compiler.apply.apply(compiler, options.plugins);
new NodeEnvironmentPlugin().apply(compiler);
NodeEnvironmentPlugin.js: compiler.plugin(“before-run”)
compiler.applyPlugins(“environment”);
compiler.applyPlugins(“after-environment”);
compiler.options = new WebpackOptionsApply().process(options, compiler);
WebpackOptionsApply.js
EntryOptionPlugin: “entry-option”
SingleEntryPlugin: “make” or MultiEntryPlugin: “make”
若干组件初始化
compiler.resolvers.context = ResolverFactory.createResolver(options.resolve)
compiler.resolvers.loader = ResolverFactory.createResolver(options.resolveLoader);
compiler.run(callback)
compiler.run(callback)
this.applyPluginsAsync(“before-run”)
this.applyPluginsAsync(“run”)
this.compile(onCompiled);
new NormalModuleFactory(this.options.context, this.resolvers, this.options.module || {})
this.applyPluginsAsync(“before-compile”)
this.applyPlugins(“compile”)
this.applyPluginsParallel(“make”)
this.applyPluginsAsync(“after-compile”)
callback(null, compilation)
new EntryOptionPlugin: ‘entry-option’
compiler.apply(‘entry-option’)
compiler.apply(new SingleEntryPlugin: “make” or MultiEntryPlugin: “make”);
SingleEntryPlugin
若干组件初始化
compiler.resolvers.context = ResolverFactory.createResolver(options.resolve)
compiler.resolvers.loader = ResolverFactory.createResolver(options.resolveLoader);
addEntry
_addModuleChain
NormalModuleFactory.create
buildModule:build-module
NormalModule.js: build
loader-runner:runLoaders
NormalModule.js: parser.parse HarmonyImportDependency 文件依赖
processModuleDependencies( 递归解析文件和处理文件依赖 )
[ HarmonyCompatibilityDependency { module: null, originModule: [Object], loc: [Object] } ], [ HarmonyImportDependency { module: null, request: 'vue', userRequest: 'vue', range: [Array], importedVar: '__WEBPACK_IMPORTED_MODULE_0_vue__', loc: [Object] } ], [ HarmonyImportDependency { module: null, request: './components/Hello.vue', userRequest: './components/Hello.vue', range: [Array], importedVar: '__WEBPACK_IMPORTED_MODULE_1__components_Hello_vue__', loc: [Object] } ], [ HarmonyImportDependency { module: null, request: './components/HelloDecorator.vue', userRequest: './components/HelloDecorator.vue', range: [Array], importedVar: '__WEBPACK_IMPORTED_MODULE_2__components_HelloDecorator_vue__', loc: [Object] } ], [ HarmonyImportSpecifierDependency { module: null, importDependency: [Object], importedVar: '__WEBPACK_IMPORTED_MODULE_0_vue__', id: 'default', name: 'Vue', range: [Array], strictExportPresence: false, namespaceObjectAsContext: false, callArgs: undefined, call: undefined, directImport: true, shorthand: undefined, loc: [Object] } ], [ HarmonyImportSpecifierDependency { module: null, importDependency: [Object], importedVar: '__WEBPACK_IMPORTED_MODULE_1__components_Hello_vue__', id: 'default', name: 'HelloComponent', range: [Array], strictExportPresence: false, namespaceObjectAsContext: false, callArgs: undefined, call: undefined, directImport: true, shorthand: undefined, loc: [Object] } ], [ HarmonyImportSpecifierDependency { module: null, importDependency: [Object], importedVar: '__WEBPACK_IMPORTED_MODULE_2__components_HelloDecorator_vue__', id: 'default', name: 'HelloDecoratorComponent', range: [Array], strictExportPresence: false, namespaceObjectAsContext: false, callArgs: undefined, call: undefined, directImport: true, shorthand: undefined, loc: [Object] } ] ]
this.plugin(“factory”)
this.plugin(“resolver”)
create(data, callback)
创建模块:
new NormalModule( result.request, ///TypeScript-Vue-Starter/node_modules/_ts-loader@3.2.0 // @ts-loader/index.js??ref--1!/TypeScript-Vue-Starter/src/index.ts result.userRequest, //'/TypeScript-Vue-Starter/src/index.ts', result.rawRequest, //'./src/index.ts' result.loaders, result.resource, result.parser);
https://github.com/webpack/enhanced-resolve/tree/master/lib
https://doc.webpack-china.org/concepts/module-resolution/
TypeScript-Vue-Starter/node_modules/enhanced-resolve/lib/ResolverFactory.js
TypeScript-Vue-Starter/node_modules/enhanced-resolve/lib/Resolver.js
TypeScript-Vue-Starter/node_modules/webpack/lib/NormalModuleFactory.js
new WebpackOptionsDefaulter().process(options);compiler.options = new WebpackOptionsApply().process(options, compiler);
class Compiler extends Tapable
Tapable.prototype.apply = function apply() { for(var i = 0; i < arguments.length; i++) { console.log('Tapable#apply', arguments[i].constructor.name); arguments[i].apply(this); }};
// name hook 事件名称// fn: function (request, callback) {// resolver.doResolve(target, obj, appending, callback);// }Tapable.prototype.plugin = function plugin(name, fn) { if(Array.isArray(name)) { name.forEach(function(name) { this.plugin(name, fn); }, this); return; } // 一个事件名可以注册多个回调函数 if(!this._plugins[name]) this._plugins[name] = [fn]; else this._plugins[name].push(fn);};
Docker GUI: https://github.com/docker/kitematic/releases
docker run -d --name gitlab-runner --restart always -v /Users/sky/dev/docker/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner:latestdocker exec -it gitlab-runner gitlab-runner register
docker pull gitlab/gitlab-runner:latest
docker stop gitlab-runner && docker rm gitlab-runner
sudo docker pull centos:latestsudo docker pull centos:centos6
sudo docker images centos
sudo docker images centos
sudo docker run centos:latest cat /etc/centos-releasesudo docker run -i -t centos7 /bin/bash
一切正常的话,你会看到一个终端提示符,然后你就可以像操作任何CentOS机器一样进行你的体验。
centos:centos7 is the image you run, for example the centos operating system image.
When you specify an image, Docker looks first for the image on your Docker host.
If the image does not exist locally, then the image is pulled from the public image
registry Docker Hub. /bin/echo is the command to run inside the new container.
docker run centos:centos7 /bin/echo 'Hello world’
The docker ps command queries the Docker daemon for information about all the
containers it knows about.
docker ps
docker logs CONTAINERID
docker run -d -P centos:centos7 python app.py
$ docker search
docker run -t -i ubuntu /bin/bash
root@00ee3e6b6450:/# apt-get install -g nodejsroot@00ee3e6b6450:/# apt-get updateroot@00ee3e6b6450:/# apt-get install -y nodejs npm gitroot@00ee3e6b6450:/# exit
docker commit -m "apt-get node install" -a "sky"
$ mkdir docker-sky-dev
$ cd docker-sky-dev
docker run -t -i centos:centos7 /bin/bash// 编译docker imagedocker build --rm -t docker-sky-centos .gitlab-ci-multi-runner unregister --url http://gitlab.xxxx.com/ci --token xxxxxxx
docker ps -a | grep "Exited" | awk '{print $1 }'|xargs docker stopdocker ps -a | grep "Exited" | awk '{print $1 }'|xargs docker rmdocker images|grep none|awk '{print $3 }'|xargs docker rmi -f
wget http://nodejs.org/dist/v6.7.0/node-v6.7.0-linux-x64.tar.gztar --strip-components 1 -xzvf node-v* -C /usr/localnode --version
wget https://github.com/cnpm/nvm/archive/v0.23.0.tar.gz -P /opt/downloadtar --strip-components 1 -xzvf /opt/download/v0.23.0.tar.gz -C /opt/downloadcd /opt/download./install.shsource ~/.nvm/nvm.shnvm install 8.13.0nvm alias default 8.13.0## install electronyum install clang dbus-devel gtk2-devel libnotify-devel libgnome-keyring-devel \ xorg-x11-server-utils libcap-devel cups-devel libXtst-devel \ alsa-lib-devel libXrandr-devel GConf2-devel nss-devel libgtk-x11-2.0.so.0 libXss.sonpm install electron -g## yum> http://repository.it4i.cz/mirrors/repoforge/redhat/el6/en/x86_64/rpmforge/RPMS/rpmforge-release-0.5.3-1.el6.rf.x86_64.rpmyum whatprovides// 安装 example.rpm 包并在安装过程中显示正在安装的文件信息及安装进度;rpm -ivh example.rpm yum groupinstall "Development Tools"dnf groupinstall "Development Tools"
https://www.centos.org/forums/viewtopic.php?t=52129
wget http://www.bzip.org/1.0.6/bzip2-1.0.6.tar.gztar xf bzip2-1.0.6.tar.gzcd bzip2-1.0.6makemake installyum install rpm-develyum install libxml2 libxslt
# 设置基础镜像FROM ubuntu:latest# 如果上个步骤已经更新软件源,这步可以忽略RUN apt-get update# 安装 NodeJS 和 npmRUN apt-get install -y nodejs npm# 将目录中的文件添加至镜像的 /srv/hello 目录中ADD . /srv/hello# 设置工作目录WORKDIR /srv/hello# 安装 Node 依赖库RUN npm install# 暴露 3001 端口,便于访问EXPOSE 3001# 设置启动时默认运行命令CMD ["nodejs”, “/srv/hello/index.js"]
]]>原文 https://hubcarl.github.io/blog/2016/09/04/react-native-debug/
在本地开发时, React Native 是加载本地Node服务, 可以通过npm start 启动, package.json 代码如下:
"scripts": { "start": "node node_modules/react-native/local-cli/cli.js start"}
加载的地址为:http://localhost:8081/debug.android.bundle?platform=android&dev=true&hot=false&minify=false
首次在电脑上面打开该地址,被庞大的源代码吓一跳。一个简单的HelloWorld App 足足有5万多行JS代码(开发模式)。仔细分析和梳理调用流程后,也没有那么的恐怖。代码主要包括React源码, 所有初始化定义的Native组件定义,Bridge层调用相关的MessageQueue,NativeModules,原生JS常用方法polyfill等代码定义实现。
如果是正式发布包,在应用运行时,是不存在本地nodejs服务器这个概念的,所以JS整合文件都是预先打包到asset资源文件里的,减少网络下载JS耗时。当然也可以从网络下载JSBundle,这时就需要考虑首次启动下载JSBundle的网络耗时和下载失败的情况处理。在项目开发时,其实可以在打包时内置一份JSBundle文件,然后启动后异步去下载最新JSBundle,下次启动时就可以加载新的JSBundle。
针对如此庞大的JSBundle文件,首次启动加载和解析的性能如何呢?
通过创建ReactInstanceManager.builder 设置setUseDeveloperSupport(true)支持远程本地调试。
远程调试时,如果是通过Android studio 打包时,可以先通过npm start启动启动本地服务,启动后服务地址:
http://localhost:8081/debug.android.bundle?platform=android&dev=true&hot=false&minify=false
如果想加载asset下的JSBundle文件,需要先把JSBundle打到本地assets目录下面,可以通过react-native bundle实现。命令自动会分析图片依赖,然后拷贝到res目录下面。
react-native bundle --entry-file ./index.android.js --bundle-output ./app/src/main/assets/index.android.jsbundle --platform android --assets-dest ./app/src/main/res/ --dev
然后setUseDeveloperSupport(false),之后重新打包即可。
在ReactInstanceManager
类里面提供了setJSBundleFile
方法,这个就是动态更新的入口.
public Builder setJSBundleFile(String jsBundleFile) { mJSBundleFile = jsBundleFile; return this; }
由于React Native加载的js文件都打包在bundle中,通过这个方法,可以设置app加载的bundle来源。若检测到远端存在更新的bundle文件,下载好后重新加载即可。
在ReactInstanceManager
类里面提供了recreateReactContextInBackground
方法, 可以通过调用该方法重新加载JSBundle文件.
private void recreateReactContextInBackground(JavaScriptExecutor jsExecutor, JSBundleLoader jsBundleLoader) { UiThreadUtil.assertOnUiThread(); ReactContextInitParams initParams = new ReactContextInitParams(jsExecutor, jsBundleLoader); if (!mIsContextInitAsyncTaskRunning) { // No background task to create react context is currently running, create and execute one. ReactContextInitAsyncTask initTask = new ReactContextInitAsyncTask(); initTask.execute(initParams); mIsContextInitAsyncTaskRunning = true; } else { // Background task is currently running, queue up most recent init params to recreate context // once task completes. mPendingReactContextInitParams = initParams; } }
目前该方法访问权限上private,需要通过反射才能调用, 希望未来 React Native 能够从官方支持. 代码如下:
private void onJSBundleLoadedFromServer() { try { Class<?> RIManagerClazz = mReactInstanceManager.getClass(); Method method = RIManagerClazz.getDeclaredMethod("recreateReactContextInBackground", JavaScriptExecutor.class, JSBundleLoader.class); method.setAccessible(true); method.invoke(mReactInstanceManager, new JSCJavaScriptExecutor(), JSBundleLoader.createFileLoader(getApplicationContext(), JS_BUNDLE_LOCAL_PATH)); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } }
React Native 增加了关键日志自定义listener回调接口MarkerListener,只要在React Activity onCreate设置ReactMarker.setMarkerListener方法,
实现MarkerListener接口logMarker方法,即可实现控制台日志打印。我们可以记录下每个关键路径的当前时间,即可计算出每个关键路径的执行时间。
ReactMarker.setMarkerListener(new ReactMarker.MarkerListener(){ @Override public void logMarker(String name) { Log.i("ReactNativeJS", name.toLowerCase() + " cost:" + System.currentTimeMillis()); }});
09-03 20:33:47.637 I/ReactNativeJS: process_packages_end cost:147238762763709-03 20:33:47.637 I/ReactNativeJS: build_native_module_registry_start cost:147238762763709-03 20:33:47.639 I/ReactNativeJS: build_native_module_registry_end cost:147238762763909-03 20:33:47.646 I/ReactNativeJS: create_catalyst_instance_start cost:147238762764609-03 20:33:47.688 I/ReactNativeJS: create_catalyst_instance_end cost:147238762768809-03 20:33:47.688 I/ReactNativeJS: run_js_bundle_start cost:147238762768809-03 20:33:47.717 I/ReactNativeJS: loadapplicationscript_startstringconvert cost:147238762771709-03 20:33:47.833 I/ReactNativeJS: loadapplicationscript_endstringconvert cost:147238762783209-03 20:33:48.787 I/ReactNativeJS: create_react_context_end cost:147238762878609-03 20:33:48.787 I/ReactNativeJS: run_js_bundle_end cost:1472387628787
render() { return ( <View style={styles.container}> <Text style={styles.welcome}> {this.state.text} </Text> <TouchableOpacity activeOpacity={0.8} onPress={this._getJSNativeCost}> <Text style={styles.instructions}> 点击我,测试JS调用Native性能 </Text> </TouchableOpacity> <TouchableOpacity activeOpacity={0.8} onPress={this._setCache}> <Text style={styles.instructions}> 点击我,设置缓存测试 </Text> </TouchableOpacity> <TouchableOpacity activeOpacity={0.8} onPress={this._getCache}> <Text style={styles.instructions}> 点击我,获取缓存值 </Text> </TouchableOpacity> <TouchableOpacity activeOpacity={0.8} onPress={this._secondActivity}> <Text style={styles.instructions}> 点击我,打开Android Native Activity页面 </Text> </TouchableOpacity> <TouchableOpacity activeOpacity={0.8} onPress={this._secondReactActivity}> <Text style={styles.instructions}> 点击我,打开Android Second React Activity页面 </Text> </TouchableOpacity> <Text style={styles.instructions}> Shake or press menu button for dev menu </Text> </View> );}
{key:'render',value:function render(){return(_react2.default.createElement(_reactNative.View,{style:styles.container},_react2.default.createElement(_reactNative.Text,{style:styles.welcome},this.state.text),_react2.default.createElement(_reactNative.TouchableOpacity,{activeOpacity:0.8,onPress:this._getJSNativeCost},_react2.default.createElement(_reactNative.Text,{style:styles.instructions},'点击我,测试JS调用Native性能')),_react2.default.createElement(_reactNative.TouchableOpacity,{activeOpacity:0.8,onPress:this._setCache},_react2.default.createElement(_reactNative.Text,{style:styles.instructions},'点击我,设置缓存测试')),_react2.default.createElement(_reactNative.TouchableOpacity,{activeOpacity:0.8,onPress:this._getCache},_react2.default.createElement(_reactNative.Text,{style:styles.instructions},'点击我,获取缓存值')),_react2.default.createElement(_reactNative.TouchableOpacity,{activeOpacity:0.8,onPress:this._secondActivity},_react2.default.createElement(_reactNative.Text,{style:styles.instructions},'点击我,打开Android Native Activity页面')),_react2.default.createElement(_reactNative.TouchableOpacity,{activeOpacity:0.8,onPress:this._secondReactActivity},_react2.default.createElement(_reactNative.Text,{style:styles.instructions},'点击我,打开Android Second React Activity页面')),_react2.default.createElement(_reactNative.Text,{style:styles.instructions},'Shake or press menu button for dev menu')));}}
09-03 20:19:19.462 Running application "SmartDebugReactApp" with appParams: {"initialProps":{},"rootTag":1}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF09-03 20:19:19.526 'JS->N : ', 8, 18, 'NaN.createView([2,"RCTView",1,{"flex":1}])'09-03 20:19:19.545 'JS->N : ', 8, 18, 'NaN.createView([3,"RCTView",1,{"collapsable":true,"flex":1}])'09-03 20:19:19.584 'JS->N : ', 28, 1, 'NaN.createTimer([2,1,1472386759583,false])'09-03 20:19:19.706 'JS->N : ', 8, 18, 'NaN.createView([4,"RCTView",1,{"flex":1,"justifyContent":"center","alignItems":"center","backgroundColor":-656129}])'09-03 20:19:19.721 'JS->N : ', 8, 18, 'NaN.createView([5,"RCTText",1,{"fontSize":20,"textAlign":"center","margin":10,"color":-65536,"accessible":true,"allowFontScaling":true,"ellipsizeMode":"tail"}])'09-03 20:19:19.732 'JS->N : ', 8, 18, 'NaN.createView([6,"RCTRawText",1,{"text":"Welcome to React Native!"}])'09-03 20:19:19.738 'JS->N : ', 8, 9, 'NaN.setChildren([5,[6]])'09-03 20:19:19.768 'JS->N : ', 8, 18, 'NaN.createView([7,"RCTView",1,{"accessible":true,"opacity":1}])'09-03 20:19:19.777 'JS->N : ', 8, 18, 'NaN.createView([8,"RCTText",1,{"textAlign":"center","color":-13421773,"marginTop":15,"marginBottom":5,"fontSize":14,"accessible":true,"allowFontScaling":true,"ellipsizeMode":"tail"}])'09-03 20:19:19.779 'JS->N : ', 8, 18, 'NaN.createView([9,"RCTRawText",1,{"text":"点击我,测试JS调用Native性能"}])'09-03 20:19:19.782 'JS->N : ', 8, 9, 'NaN.setChildren([8,[9]])'09-03 20:19:19.783 'JS->N : ', 8, 9, 'NaN.setChildren([7,[8]])'09-03 20:19:19.801 'JS->N : ', 8, 18, 'NaN.createView([10,"RCTView",1,{"accessible":true,"opacity":1}])'09-03 20:19:19.810 'JS->N : ', 8, 18, 'NaN.createView([12,"RCTText",1,{"textAlign":"center","color":-13421773,"marginTop":15,"marginBottom":5,"fontSize":14,"accessible":true,"allowFontScaling":true,"ellipsizeMode":"tail"}])'09-03 20:19:19.812 'JS->N : ', 8, 18, 'NaN.createView([13,"RCTRawText",1,{"text":"点击我,设置缓存测试"}])'09-03 20:19:19.813 'JS->N : ', 8, 9, 'NaN.setChildren([12,[13]])'09-03 20:19:19.814 'JS->N : ', 8, 9, 'NaN.setChildren([10,[12]])'09-03 20:19:19.834 'JS->N : ', 8, 18, 'NaN.createView([14,"RCTView",1,{"accessible":true,"opacity":1}])'09-03 20:19:19.849 'JS->N : ', 8, 18, 'NaN.createView([15,"RCTText",1,{"textAlign":"center","color":-13421773,"marginTop":15,"marginBottom":5,"fontSize":14,"accessible":true,"allowFontScaling":true,"ellipsizeMode":"tail"}])'09-03 20:19:19.851 'JS->N : ', 8, 18, 'NaN.createView([16,"RCTRawText",1,{"text":"点击我,获取缓存值"}])'09-03 20:19:19.851 'JS->N : ', 8, 9, 'NaN.setChildren([15,[16]])'09-03 20:19:19.854 'JS->N : ', 8, 9, 'NaN.setChildren([14,[15]])'09-03 20:19:19.881 'JS->N : ', 8, 18, 'NaN.createView([17,"RCTView",1,{"accessible":true,"opacity":1}])'09-03 20:19:19.890 'JS->N : ', 8, 18, 'NaN.createView([18,"RCTText",1,{"textAlign":"center","color":-13421773,"marginTop":15,"marginBottom":5,"fontSize":14,"accessible":true,"allowFontScaling":true,"ellipsizeMode":"tail"}])'09-03 20:19:19.894 'JS->N : ', 8, 18, 'NaN.createView([19,"RCTRawText",1,{"text":"点击我,打开Android Native Activity页面"}])'09-03 20:19:19.895 'JS->N : ', 8, 9, 'NaN.setChildren([18,[19]])'09-03 20:19:19.896 'JS->N : ', 8, 9, 'NaN.setChildren([17,[18]])'09-03 20:19:19.914 'JS->N : ', 8, 18, 'NaN.createView([20,"RCTView",1,{"accessible":true,"opacity":1}])'09-03 20:19:19.924 'JS->N : ', 8, 18, 'NaN.createView([22,"RCTText",1,{"textAlign":"center","color":-13421773,"marginTop":15,"marginBottom":5,"fontSize":14,"accessible":true,"allowFontScaling":true,"ellipsizeMode":"tail"}])'09-03 20:19:19.927 'JS->N : ', 8, 18, 'NaN.createView([23,"RCTRawText",1,{"text":"点击我,打开Android Second React Activity页面"}])'09-03 20:19:19.932 'JS->N : ', 8, 9, 'NaN.setChildren([22,[23]])'09-03 20:19:19.935 'JS->N : ', 8, 9, 'NaN.setChildren([20,[22]])'09-03 20:19:19.941 'JS->N : ', 8, 18, 'NaN.createView([24,"RCTText",1,{"textAlign":"center","color":-13421773,"marginTop":15,"marginBottom":5,"fontSize":14,"accessible":true,"allowFontScaling":true,"ellipsizeMode":"tail"}])'09-03 20:19:19.945 'JS->N : ', 8, 18, 'NaN.createView([25,"RCTRawText",1,{"text":"Shake or press menu button for dev menu"}])'09-03 20:19:19.946 'JS->N : ', 8, 9, 'NaN.setChildren([24,[25]])'09-03 20:19:19.950 'JS->N : ', 8, 9, 'NaN.setChildren([4,[5,7,10,14,17,20,24]])'09-03 20:19:19.951 'JS->N : ', 8, 9, 'NaN.setChildren([3,[4]])'09-03 20:19:19.962 'JS->N : ', 8, 18, 'NaN.createView([26,"RCTView",1,{"collapsable":true,"position":"absolute"}])'09-03 20:19:19.963 'JS->N : ', 8, 9, 'NaN.setChildren([2,[3,26]])'09-03 20:19:19.964 'JS->N : ', 8, 9, 'NaN.setChildren([1,[2]])'09-03 20:19:19.976 'JS->N : ', 24, 0, 'NaN.getDataFromIntent([0,1])'09-03 20:19:19.978 'JS->N : ', 1, 1, 'NaN.show(["Toast 是原生支持的!",3000])'09-03 20:19:20.056 'JS->N : ', 8, 12, 'NaN.updateView([6,"RCTRawText",{"text":"注意:数据为空!"}])'
准备三组测试数据:
第一组(简单): key: 随机生成 value: 我是来自React Native缓存消息
第二组(长字符): key: 随机生成 value: 我是来自React Native缓存消息(循环50遍)
第二组(JSON): key: 随机生成 value: 下面JSON字符串,循环10遍, 内容不重复
{
“id”: 000001,
“title”: “React Native接口性能测试”,
“summary”: “炫斗不停,精彩不断,不要怂就是干的全武将萌化翻转扮演的新式三国策略养成手游《女神三国》邀您…”,
“category”: “React Native”,
“createTime”: “2016-09-09 17:48:38”,
“publicTime”: “2016-09-09 17:48:00”
}
@ReactMethodpublic void setCache(String key, String value, Callback successCallback, Callback errorCallback) { try { sharedPreference = getCurrentActivity().getSharedPreferences("rn_cache", 0); sharedPreference.edit().putString(key, value).commit(); successCallback.invoke("save success"); } catch (Exception e) { e.printStackTrace(); errorCallback.invoke(e.getMessage()); }}//Java中的方法需要导出才能给JS使用,要导出Java方法,需要使用@ReactMethod来注解,且方法的返回值只能是void。@ReactMethodpublic void getCache(String key, Callback callback) { callback.invoke(sharedPreference.getString(key, ""));}
const start = +new Date();NativeModules.IntentModule.getCache('RN001',(value)=>{ const time = +new Date()-start; console.log('>>>>cost[getCache]:', time); NativeModules.ToastAndroid.show(value+' cost:'+ time, 3000)});
Native收到JS传递过来的值直接返回给JS, 经过多次对三组进行测试(Nexus 5 Android 5.0, MX3 5.0),时间稳定在2-4ms, 偶尔会出现5ms, 数据的大小对接口调用耗时影响不大.
@JavascriptInterfacepublic void setCache(String key, String value) { try { sharedPreference = context.getSharedPreferences("rn_cache", 0); sharedPreference.edit().putString(key, value).commit(); } catch (Exception e) { e.printStackTrace(); }}@JavascriptInterfacepublic String getCache(String key) { return sharedPreference.getString(key, "");}
function getCache() { var start = +new Date(); var ret = HybridApp.getCache('RN001'); var end = +new Date(); var str = '>>>cost[getCache]:' + (end - start) + ' result:' + ret; console.log(str);}
JS从Native获取数据, 经过多次进行三组数据测试(Nexus 5 Android 5.0),时间稳定在0-3ms, 多次点击后,时间更短,时间稳定范围0s-1s,说明Interface有缓存机制和数据的大小对接口调用耗时影响不大.
@Overridepublic boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { result.confirm(message); return true;}
经过多次对三组进行数据进行测试(Nexus 5 Android 5.),时间稳定在1-2ms,数据的大小对接口调用耗时影响不大
从测试效果来看, 三种方式接口调用耗时都在1s-4s级别, 性能表现都非常不错. React Native因为进行了接口封装转换, 比addJavascriptInterface和prompt方式都是简单的数据透传返回要慢1ms-2ms是可以预期的.
说明: 这里只是简单的接口调用测试, 当前运行环境(Native线程切换,Native数据获取方式,数据回调方式等)都可能会影响实际的接口调用耗时.
第一次测试
09-08 20:41:39.002 I/ReactNativeJS﹕ >>>react performance react start:147333849900209-08 20:41:39.081 I/ReactNativeJS﹕ >>>react performance react end:147333849908109-08 20:41:39.601 I/ReactNativeJS﹕ >>>react[runApplication]:147333849960009-08 20:41:39.618 I/ReactNativeJS﹕ >>>react#constructor, 147333849961609-08 20:41:39.618 I/ReactNativeJS﹕ >>>react#componentWillMount, 147333849961809-08 20:41:39.711 I/ReactNativeJS﹕ >>>react#componentDidMount, 1473338499711
cost:1473338499711-1473338499002=709ms
第二次测试
09-08 20:45:42.774 I/ReactNativeJS﹕ >>>react performance react start:147333874277409-08 20:45:42.806 I/ReactNativeJS﹕ >>>react performance react end:147333874280609-08 20:45:43.300 14935-14965/com.react.smart I/ReactNativeJS﹕ >>>react[runApplication]:147333874329909-08 20:45:43.320 14935-14965/com.react.smart I/ReactNativeJS﹕ >>>react#constructor, 147333874331909-08 20:45:43.321 14935-14965/com.react.smart I/ReactNativeJS﹕ >>>react#componentWillMount, 147333874332109-08 20:45:43.471 14935-14965/com.react.smart I/ReactNativeJS﹕ >>>react#componentDidMount, 1473338743471
cost:1473338743471-1473338742774=697ms
第三次测试
09-08 20:41:39.002 I/ReactNativeJS﹕ >>>react performance react start:147333849900209-08 20:41:39.081 I/ReactNativeJS﹕ >>>react performance react end:147333849908109-08 20:41:39.601 I/ReactNativeJS﹕ >>>react[runApplication]:147333849960009-08 20:41:39.618 I/ReactNativeJS﹕ >>>react#constructor, 147333849961609-08 20:41:39.618 I/ReactNativeJS﹕ >>>react#componentWillMount, 147333849961809-08 20:41:39.711 I/ReactNativeJS﹕ >>>react#componentDidMount, 1473338499711
cost:1473338499711-1473338499002=709ms
第四次测试
09-08 20:50:46.781 I/ReactNativeJS﹕ >>>react performance react start:147333904678109-08 20:50:46.789 I/ReactNativeJS﹕ >>>react performance react end:147333904678909-08 20:50:47.213 I/ReactNativeJS﹕ >>>react[runApplication]:147333904721309-08 20:50:47.231 I/ReactNativeJS﹕ >>>react#constructor, 147333904722909-08 20:50:47.231 I/ReactNativeJS﹕ >>>react#componentWillMount, 147333904723109-08 20:50:47.327 I/ReactNativeJS﹕ >>>react#componentDidMount, 1473339047327
cost:1473339047327-1473339046781=546ms
从测试结果来看, Nexus5 时间稳定在500ms-700ms之间, 时间可以接受.
第一次测试
09-11 16:51:36.967 I/ReactNativeJS﹕ >>>react performance react start:147358389696709-11 16:51:37.091 I/ReactNativeJS﹕ >>>react performance react end:147358389709109-11 16:51:38.349 I/ReactNativeJS﹕ '>>>react#constructor', 147358389834209-11 16:51:38.350 I/ReactNativeJS﹕ '>>>react#componentWillMount', 147358389834909-11 16:51:38.523 I/ReactNativeJS﹕ '>>>react#componentDidMount', 147358389852309-11 16:51:38.528 I/ReactNativeJS﹕ '>>>react#componentDidMount#ToastAndroid.show', 1473583898527
cost:1473583898527-1473583896967=1560ms
第二次测试
09-11 16:53:48.688 I/ReactNativeJS﹕ >>>react performance react start:147358402868809-11 16:53:48.887 I/ReactNativeJS﹕ >>>react performance react end:147358402888709-11 16:53:50.345 I/ReactNativeJS﹕ '>>>react#constructor', 147358403034209-11 16:53:50.346 I/ReactNativeJS﹕ '>>>react#componentWillMount', 147358403034509-11 16:53:50.500 I/ReactNativeJS﹕ '>>>react#componentDidMount', 147358403050009-11 16:53:50.504 I/ReactNativeJS﹕ '>>>react#componentDidMount#ToastAndroid.show', 1473584030503
cost:1473584030503-1473584028688=1815ms
第三次测试
09-11 17:10:20.694 I/ReactNativeJS﹕ >>>react performance react start:147358502069409-11 17:10:20.894 I/ReactNativeJS﹕ >>>react performance react end:147358502089409-11 17:10:22.225 I/ReactNativeJS﹕ '>>>react#constructor', 147358502222209-11 17:10:22.226 I/ReactNativeJS﹕ '>>>react#componentWillMount', 147358502222509-11 17:10:22.405 I/ReactNativeJS﹕ '>>>react#componentDidMount', 147358502240509-11 17:10:22.409 I/ReactNativeJS﹕ '>>>react#componentDidMount#ToastAndroid.show', 1473585022408
cost:1473585022408-1473585020694=1714ms
第四次测试
09-11 17:11:25.690 I/ReactNativeJS﹕ >>>react performance react start:147358508569009-11 17:11:25.865 I/ReactNativeJS﹕ >>>react performance react end:147358508586509-11 17:11:27.173 I/ReactNativeJS﹕ '>>>react#constructor', 147358508716909-11 17:11:27.173 I/ReactNativeJS﹕ '>>>react#componentWillMount', 147358508717309-11 17:11:27.336 I/ReactNativeJS﹕ '>>>react#componentDidMount', 147358508733509-11 17:11:27.340 I/ReactNativeJS﹕ '>>>react#componentDidMount#ToastAndroid.show', 1473585087339
cost: 1473585087339-1473585085690=1649ms
从测试结果来看, MX3 时间稳定在1500ms-1800ms之间, 明显比Nexus5要慢.
内存占用曲线图
从曲线图看出内存占用非常稳定, 一个HellWord的React Native App占用内存大概在20M
cpu曲线图
从曲线图看出启动的时候cpu瞬间飙到40%, 原因是因为启动时涉及Android和React Native JS与Native的大量调用,这个可以从上面View的绘制的过程可以看出.
第二个cpu波动是我这边频繁的点击[点击我]相关测试, 停止点击后, cpu马上就降落下来.
]]>在用 Node.js + Webpack 构建的方式进行开发时, 我们希望能实现修改代码能实时刷新页面UI的效果. 这个特性 Webpack本身是支持的, 而且基于koa也有现成的 koa-webpack-hot-middleware 和 koa-webpack-dev-middleware 封装好的组件支持.
不过这里如果需要支持Node.js服务器端修改代码自动重启webpack自动编译功能该如何实现呢, 主要存在以下几个问题:
前端渲染和服务端渲染构建热更新实现
在koa项目中, 通过 koa-webpack-dev-middleware 和 koa-webpack-hot-middleware 可以实现 Webpack 编译内存存储和热更新功能, 代码如下:
const compiler = webpack(webpackConfig);const devMiddleware = require('koa-webpack-dev-middleware')(compiler, options);const hotMiddleware = require('koa-webpack-hot-middleware')(compiler, options);app.use(devMiddleware);app.use(hotMiddleware);
如果按照上面实现, 可以满足修改修改客户端代码实现webpack自动变编译和UI界面热更新的功能.
但如果是修改 Node.js 服务器端代码重启后就会发现webpack会重新编译, 这不是我们要的效果.
原因是因为 middleware 是依赖 app 的生命周期, 当 app 销毁时, 对应 Webpack compiler 实例也就没有了, 重启时会重新执行middleware 初始化工作.
那有没有办法保持 Webpack 编译实例呢? 针对这个我们可以通过 Egg 框架已经内置了 worke r和 agent 机制来实现 Webpack内存编译功能.
node index.js
或者 npm run dev
启动 Egg 应用app.vue.renderBundle = (name, context, options) => { const filePath = path.isAbsolute(name) ? name : path.join(app.config.view.root[0], name); const promise = app.webpack.fileSystem.readWebpackMemoryFile(filePath, name); return co(function* () { const content = yield promise; if (!content) { throw new Error(`read webpack memory file[${filePath}] content is empty, please check if the file exists`); } return renderBundle.bind(app.vue)(content, context, options); });};
浏览器输入URL请求地址, 然后Egg接收到请求, 然后进入Controller
Node层获取数据后(Node通过http/rpc方式调用Java后端API数据接口), 进入模板render流程
进入render流程后, 通过worker进程通过调用 app.messenger.sendToAgent
发送文件名给Agent进程, 同时通过 app.messenger.on
启动监听监听agent发送过来的消
Agent进程获取到文件名后, 从Webpack编译内存里面获取文件内容, 然后Agent 通过 agent.messenger.sendToApp
把文件内容发送给Worker进程
Worker进程获取到内容以后, 进行Vue编译HTML, 编译成HTML后, 进入jss/css资源依赖流程
如果启动代理模式(见easywebpack的setProxy), HTML直接注入相对路径的JS/CSS, 如下:
页面可以直接使用 /public/client/js/vendor.js
相对路径, /public/client/js/vendor.js
由后端框架代理转发到webpack编译服务, 然后返回内容给后端框架, 这里涉及两个应用通信. 如下:
<link rel="stylesheet" href="/public/client/css/home/android/home.css">
如果非代理模式, HTML直接注入必须是绝对路径的 JS/CSS , 如下:
页面必须使用
http://127.0.0.1:9001/public/client/js/vendor.js
绝对路径
其中 http://127.0.0.1:9001 是 Agent里面启动的Webpack编译服务地址, 与Egg应用地址是两回事
最后, 模板渲染完成, 服务器输出HTML内容给浏览器.
easy build prod
manfifest.json
文件注入 jss/css 资源依赖注入.原文:https://hubcarl.github.io/blog/2016/09/15/react-native-update/
React Native 动态更新实际效果如下
我们知道, React Native所有的js文件都打包在一个jsbundle文件中,发布时也是打包到app里面,一般是放到asset目录.
如是猜想是不是可以从远程下载jsbundle文件覆盖asset的jsbundle. 查资料发现asset目录是只读的,该想法行不通.
在看React Native 启动入口时,看到通过是setBundleAssetName指定 asset文件的, 查看方法实现:
public ReactInstanceManager.Builder setBundleAssetName(String bundleAssetName) { return this.setJSBundleFile(bundleAssetName == null?null:"assets://" + bundleAssetName);}
发现调用了setJSBundleFile方法, 而且该方法是public的, 也就是可以通过这个方法指定的jsbundle文件
public ReactInstanceManager.Builder setJSBundleFile(String jsBundleFile) { this.mJSBundleFile = jsBundleFile; this.mJSBundleLoader = null; return this;}
可以设置了jsbundle文件, 那我们就可以把jsbundle文件放到sdcard, 经过测试发现, 确实可以读取sdcard jsbundle.
sdcar的文件开业读取了,那我们就可以把文件放到远程服务器, 启动后下载远程jsbundle文件到sdcard. 大概思路如下:
我们打好包jsbundle文件放到远程服务器
启动React Native, 检查sdcard是否有jsbundle文件, 如果没有调用setBundleAssetName加载asset目录的jsbundle, 同时启动线程下载远程jsbundle文件到sdcard目录.
待下次启动时, sdcard是有jsbundle文件的, 加载的就是最新的jsbundle文件.
实现代码如下:
public static final String JS_BUNDLE_REACT_UPDATE_PATH = Environment.getExternalStorageDirectory().toString() + File.separator + "react_native_update/debug.android.bundle";private void iniReactRootView(boolean isRelease) { ReactInstanceManager.Builder builder = ReactInstanceManager.builder() .setApplication(getApplication()) .setJSMainModuleName("debug.android.bundle") .addPackage(new MainReactPackage()) .addPackage(new Package()) .setInitialLifecycleState(LifecycleState.RESUMED); File file = new File(JS_BUNDLE_LOCAL_PATH); if (isRelease && file != null && file.exists()) { builder.setJSBundleFile(JS_BUNDLE_LOCAL_PATH); Log.i(TAG, "load bundle from local cache"); } else { builder.setBundleAssetName(JS_BUNDLE_LOCAL_FILE); Log.i(TAG, "load bundle from asset"); updateJSBundle(); } mReactRootView = new ReactRootView(this); mReactInstanceManager = builder.build(); mReactRootView.startReactApplication(mReactInstanceManager, "SmartReactApp", null); setContentView(mReactRootView);}// 从远程服务器下载新的jsbundle文件private void updateJSBundle() { DownloadManager.Request request = new DownloadManager.Request( Uri.parse(JS_BUNDLE_REMOTE_URL)); request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI); request.setDestinationUri(Uri.parse("file://" + JS_BUNDLE_LOCAL_PATH)); DownloadManager dm = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); mDownloadId = dm.enqueue(request); Log.i(TAG, "start download remote js bundle file");}
经过测试发现, 确实可以实现动态更新, 但要下次启动才能看到最新的效果, 那有没有办法实现立即看到更新效果呢?
通过查看React Native 源码和查阅资料是可以实现的, 具体实现如下:
为了在运行中重新加载bundle文件,查看ReactInstanceManager的源码,找到如下方法:
private void recreateReactContextInBackground(JavaScriptExecutor jsExecutor, JSBundleLoader jsBundleLoader) { UiThreadUtil.assertOnUiThread(); ReactContextInitParams initParams = new ReactContextInitParams(jsExecutor, jsBundleLoader); if (!mIsContextInitAsyncTaskRunning) { // No background task to create react context is currently running, create and execute one. ReactContextInitAsyncTask initTask = new ReactContextInitAsyncTask(); initTask.execute(initParams); mIsContextInitAsyncTaskRunning = true; } else { // Background task is currently running, queue up most recent init params to recreate context // once task completes. mPendingReactContextInitParams = initParams; }}
虽然这个方法是private的,但是可以通过反射调用,下面是0.29版本的实现(上面React-Native-Remote-Update项目实现React Native版本旧了,直接拷贝反射参数有问题)
private void onJSBundleLoadedFromServer() { File file = new File(JS_BUNDLE_LOCAL_PATH); if (file == null || !file.exists()) { Log.i(TAG, "js bundle file download error, check URL or network state"); return; } Log.i(TAG, "js bundle file file success, reload js bundle"); Toast.makeText(UpdateReactActivity.this, "download bundle complete", Toast.LENGTH_SHORT).show(); try { Class<?> RIManagerClazz = mReactInstanceManager.getClass(); Field f = RIManagerClazz.getDeclaredField("mJSCConfig"); f.setAccessible(true); JSCConfig jscConfig = (JSCConfig)f.get(mReactInstanceManager); Method method = RIManagerClazz.getDeclaredMethod("recreateReactContextInBackground", com.facebook.react.cxxbridge.JavaScriptExecutor.Factory.class, com.facebook.react.cxxbridge.JSBundleLoader.class); method.setAccessible(true); method.invoke(mReactInstanceManager, new com.facebook.react.cxxbridge.JSCJavaScriptExecutor.Factory(jscConfig.getConfigMap()), com.facebook.react.cxxbridge.JSBundleLoader.createFileLoader( getApplicationContext(), JS_BUNDLE_LOCAL_PATH)); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (NoSuchFieldException e){ e.printStackTrace(); }}
通过监听下载成功事件, 然后调用onJSBundleLoadedFromServer接口就可以看到立即更新的效果.
private CompleteReceiver mDownloadCompleteReceiver;private long mDownloadId;private void initDownloadManager() { mDownloadCompleteReceiver = new CompleteReceiver(); registerReceiver(mDownloadCompleteReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));}private class CompleteReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); if (completeDownloadId == mDownloadId) { onJSBundleLoadedFromServer(); } }}
尝试以后果然可以更新, 当时心情非常好~ 可是……, 后面继续实现项目时发现, 动态更新后, 本地图片始终不显示, 远程图片可以.
接下来查看React Native, jsbundle 源码和查看资料, 终于寻的一点蛛丝马迹, 大概的意思如下:
如果bundle在sd卡【 比如bundle在file://sdcard/react_native_update/index.android.bundle 那么图片目录在file://sdcard/react_native_update/drawable-mdpi】
如果你的bundle在assets里,图片资源要放到res文件夹里,例如res/drawable-mdpi
接下来按照该说法进行了实验, 发现确实可以. 当界面刷新时,心情格外好! 下面是详细代码实现(部分代码参考React-Native-Remote-Update项目,在这里直接引用):
package com.react.smart;import android.app.Activity;import android.app.DownloadManager;import android.content.BroadcastReceiver;import android.content.Context;import android.content.Intent;import android.content.IntentFilter;import android.net.Uri;import android.os.Bundle;import android.os.Environment;import android.util.Log;import android.view.KeyEvent;import android.widget.Toast;import com.facebook.react.JSCConfig;import com.facebook.react.LifecycleState;import com.facebook.react.ReactInstanceManager;import com.facebook.react.ReactRootView;import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;import com.facebook.react.shell.MainReactPackage;import com.react.smart.componet.Package;import com.react.smart.utils.FileAssetUtils;import java.io.File;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;/** * Created by sky on 16/7/15. * https://github.com/hubcarl *//** * Created by sky on 16/9/4. * */public class UpdateReactActivity extends Activity implements DefaultHardwareBackBtnHandler { private static final String TAG = "UpdateReactActivity"; public static final String JS_BUNDLE_REMOTE_APP= "https://raw.githubusercontent.com/hubcarl/smart-react-native-app"; public static final String JS_BUNDLE_REMOTE_URL = JS_BUNDLE_REMOTE_APP + "/debug/app/src/main/assets/index.android.bundle"; public static final String JS_BUNDLE_LOCAL_FILE = "debug.android.bundle"; public static final String JS_BUNDLE_REACT_UPDATE_PATH = Environment.getExternalStorageDirectory().toString() + File.separator + "react_native_update"; public static final String JS_BUNDLE_LOCAL_PATH = JS_BUNDLE_REACT_UPDATE_PATH + File.separator + JS_BUNDLE_LOCAL_FILE; private ReactInstanceManager mReactInstanceManager; private ReactRootView mReactRootView; private CompleteReceiver mDownloadCompleteReceiver; private long mDownloadId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); iniReactRootView(true); initDownloadManager(); updateJSBundle(true); } // 如果bundle在sd卡【 比如bundle在file://sdcard/react_native_update/index.android.bundle // 那么图片目录在file://sdcard/react_native_update/drawable-mdpi】 // 如果你的bundle在assets里,图片资源要放到res文件夹里,例如res/drawable-mdpi private void iniReactRootView(boolean isRelease) { ReactInstanceManager.Builder builder = ReactInstanceManager.builder() .setApplication(getApplication()) .setJSMainModuleName(JS_BUNDLE_LOCAL_FILE) .addPackage(new MainReactPackage()) .addPackage(new Package()) .setInitialLifecycleState(LifecycleState.RESUMED); File file = new File(JS_BUNDLE_LOCAL_PATH); if (isRelease && file != null && file.exists()) { builder.setJSBundleFile(JS_BUNDLE_LOCAL_PATH); Log.i(TAG, "load bundle from local cache"); } else { builder.setBundleAssetName(JS_BUNDLE_LOCAL_FILE); Log.i(TAG, "load bundle from asset"); } mReactRootView = new ReactRootView(this); mReactInstanceManager = builder.build(); mReactRootView.startReactApplication(mReactInstanceManager, "SmartReactApp", null); setContentView(mReactRootView); } private void updateJSBundle(boolean isRelease) { File file = new File(JS_BUNDLE_LOCAL_PATH); if (isRelease && file != null && file.exists()) { Log.i(TAG, "new bundle exists !"); return; } File rootDir = new File(JS_BUNDLE_REACT_UPDATE_PATH); if (rootDir != null && !rootDir.exists()) { rootDir.mkdir(); } File res = new File(JS_BUNDLE_REACT_UPDATE_PATH + File.separator + "drawable-mdpi"); if (res != null && !res.exists()) { res.mkdir(); } FileAssetUtils.copyAssets(this, "drawable-mdpi", JS_BUNDLE_REACT_UPDATE_PATH); DownloadManager.Request request = new DownloadManager.Request(Uri.parse(JS_BUNDLE_REMOTE_URL)); request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI); request.setDestinationUri(Uri.parse("file://" + JS_BUNDLE_LOCAL_PATH)); DownloadManager dm = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); mDownloadId = dm.enqueue(request); Log.i(TAG, "start download remote js bundle file"); } private void initDownloadManager() { mDownloadCompleteReceiver = new CompleteReceiver(); registerReceiver(mDownloadCompleteReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); } private class CompleteReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); if (completeDownloadId == mDownloadId) { onJSBundleLoadedFromServer(); } } } private void onJSBundleLoadedFromServer() { File file = new File(JS_BUNDLE_LOCAL_PATH); if (file == null || !file.exists()) { Log.i(TAG, "js bundle file download error, check URL or network state"); return; } Log.i(TAG, "js bundle file file success, reload js bundle"); Toast.makeText(UpdateReactActivity.this, "download bundle complete", Toast.LENGTH_SHORT).show(); try { Class<?> RIManagerClazz = mReactInstanceManager.getClass(); Field f = RIManagerClazz.getDeclaredField("mJSCConfig"); f.setAccessible(true); JSCConfig jscConfig = (JSCConfig)f.get(mReactInstanceManager); Method method = RIManagerClazz.getDeclaredMethod("recreateReactContextInBackground", com.facebook.react.cxxbridge.JavaScriptExecutor.Factory.class, com.facebook.react.cxxbridge.JSBundleLoader.class); method.setAccessible(true); method.invoke(mReactInstanceManager, new com.facebook.react.cxxbridge.JSCJavaScriptExecutor.Factory( jscConfig.getConfigMap() ), com.facebook.react.cxxbridge.JSBundleLoader.createFileLoader( getApplicationContext(), JS_BUNDLE_LOCAL_PATH) ); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (NoSuchFieldException e){ e.printStackTrace(); } } @Override protected void onDestroy() { super.onDestroy(); unregisterReceiver(mDownloadCompleteReceiver); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) { mReactInstanceManager.showDevOptionsDialog(); return true; } return super.onKeyUp(keyCode, event); } @Override public void onBackPressed() { if (mReactInstanceManager != null) { mReactInstanceManager.onBackPressed(); } else { super.onBackPressed(); } } @Override public void invokeDefaultOnBackPressed() { super.onBackPressed(); } @Override protected void onPause() { super.onPause(); } @Override protected void onResume() { super.onResume(); }}
asset资源文件拷贝到sdcard, 当然实际实现时, 资源文件和jsbundle文件可以都应该放到远程服务器.
package com.react.smart.utils;import android.content.Context;import android.content.res.AssetManager;import android.util.Log;import java.io.*;/** * Created by sky on 16/9/19. */public class FileAssetUtils { public static void copyAssets(Context context, String src, String dist) { AssetManager assetManager = context.getAssets(); String[] files = null; try { files = assetManager.list(src); } catch (IOException e) { Log.e("tag", "Failed to get asset file list.", e); } for(String filename : files) { InputStream in = null; OutputStream out = null; try { in = assetManager.open(src + File.separator + filename); File outFile = new File(dist + File.separator + src, filename); out = new FileOutputStream(outFile); copyFile(in, out); in.close(); in = null; out.flush(); out.close(); out = null; } catch(IOException e) { Log.e("tag", "Failed to copy asset file: " + filename, e); } } } public static void copyFile(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[1024]; int read; while((read = in.read(buffer)) != -1){ out.write(buffer, 0, read); } }}
最后附上github项目地址:https://github.com/hubcarl/smart-react-native-app,欢迎follow!
]]>原文:https://hubcarl.github.io/blog/2016/08/28/react-native-js/
首先来看一下一张完整Native与JavaScript交互原理图(来自网络,现找不到原地址了):
在 React Native App中,在应用启动时根据 ReactPackage 会自动生成 JavaScriptModuleRegistry和NativeModuleRegistry两份模块配置表,包含系统CoreModulesPackage, 基础模块MainReactPackage以及自定义模块。Java端与JavaScript端持有相同的模块配置表,标识为可识别为Native模块或JavaScript模块都是通过实现相应接口,并将实例添加ReactPackage的CreactModules方法即可。
JavaScript模块extends JavascriptModule, JavaScript模块通过java动态代理实现调用Js模块。下例 AppRegistry.java 为在加载完 Jsbundle 后,Native 去启动 React Application 的总入口,appkey 为应用的 ID。映射每个 JavascriptModule 的信息保存在 JavaScriptModuleRegistration 中,统一由 JavaScriptModuleRegistry统一管理。
ReactInstanceManagerImpl.java 中 ReactContextInitAsyncTask.setupReactContext() 方法中如下调用:
((AppRegistry)catalystInstance.getJSModule(AppRegistry.class)).runApplication(appkey, appParams);
JS模块通过java动态代理实现调用JS方法,java动态代理通过实现InvocationHandler接口,然后通过Proxy.newProxyInstance()实现. 方法有三个参数:
类加载器(Class Loader)
需要实现的接口数组
所有动态代理类的方法调用,都会交由InvocationHandler接口实现类里的invoke()方法去处理。这是动态代理的关键所在。
(JavaScriptModule)Proxy.newProxyInstance(moduleInterface.getClassLoader(), new Class[]{moduleInterface},new JavaScriptModuleRegistry.JavaScriptModuleInvocationHandler( executorToken, instance, registration));
public interface AppRegistry extends JavaScriptModule { void runApplication(String appKey, WritableMap appParameters); void unmountApplicationComponentAtRootTag(int rootNodeTag); }
private static class JavaScriptModuleInvocationHandler implements InvocationHandler { @Nullable public Object invoke(Object proxy, Method method, @Nullable Object[] args) { ExecutorToken executorToken = (ExecutorToken)this.mExecutorToken.get(); if(executorToken == null) { FLog.w("React", "Dropping JS call, ExecutorToken went away..."); return null; } else { String tracingName = this.mModuleRegistration.getTracingName(method); WritableNativeArray jsArgs = args != null?Arguments.fromJavaArgs(args):new WritableNativeArray(); this.mCatalystInstance.callFunction(executorToken, this.mModuleRegistration.getName(), method.getName(), jsArgs, tracingName); return null; } }}
static void callFunction(JNIEnv* env, jobject obj, JExecutorToken::jhybridobject jExecutorToken, jstring module, jstring method,
void Bridge::callFunction(ExecutorToken executorToken,const std::string& moduleId,const std::string& methodId,const folly::dynamic& arguments,const std::string& tracingName)
1. void JSCExecutor::callFunction(const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments) // 确保fbBatchedBridge 有定义 2. bool JSCExecutor::ensureBatchedBridgeObject() // 执行fbBatchedBridge中js方法 3. void JSCExecutor::callFunction(const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments) { 4. 执行js fbBatchedBridge.callFunctionReturnFlushedQueue 返回queue队列 5. 执行Bridge.cpp : void Bridge::callNativeModules(JSExecutor& executor, const std::string& callJSON, bool isEndOfBatch) 6. 执行 m_callback->onCallNativeModules(getTokenForExecutor(executor), callJSON, isEndOfBatch); m_callback 为OnLoad.cpp 中的 class PlatformBridgeCallback : public BridgeCallback 相当于执行 PlatformBridgeCallback.onCallNativeModules 7. 最后调用 makeJavaCall方法调用java方法 8. OnLoad.cpp 中 makeJavaCall 定义, c++通过CallVoidMethod调用java非静态方法: gCallbackMethod 定义: jclass callbackClass = env->FindClass("com/facebook/react/bridge/ReactCallback"); bridge::gCallbackMethod = env->GetMethodID(callbackClass, "call", "(Lcom/facebook/react/bridge/ExecutorToken;IILcom/facebook/react/bridge/ReadableNativeArray;)V"); static void makeJavaCall(JNIEnv* env, ExecutorToken executorToken, jobject callback, const MethodCall& call) { auto newArray = ReadableNativeArray::newObjectCxxArgs(std::move(call.arguments)); env->CallVoidMethod( callback, gCallbackMethod, static_cast<JExecutorTokenHolder*>( executorToken.getPlatformExecutorToken().get())->getJobj(), call.moduleId, call.methodId, newArray.get()); }
在JSBundle.js文件底部有两个require调用:
require(191); // require(‘InitializeJavaScriptAppEngine’)
InitializeJavaScriptAppEngine 初始化,主要包括Map,Set,XHR, Timer, Log,Fetch,WebSocket PolyfillRCTDeviceEventEmitter,RCTNativeAppEventEmitter,PerformanceLogger初始化
require(0); // require(‘SmartRectNativeApp/debug.android.js’)
JS启动入口,其中会引用require('react') 和 require('react-native')ReactNative.AppRegistry.registerComponent('SmartDebugReactApp', function () { return SmartRectNativeApp;});
下面具体梳理一下require(0)后启动流程
通过MessageQueue定义RemoteModules对象
function MessageQueue(configProvider) { lazyProperty(this, 'RemoteModules', function () { var _configProvider =configProvider(); var remoteModuleConfig = _configProvider.remoteModuleConfig; var modulesConfig = this._genModulesConfig(remoteModuleConfig); // 初始化所有JS调用Native模块 var modules = this._genModules(modulesConfig); return modules; }); }
__fbBatchedBridgeConfig 由Native层注入的全局对象,数据格式如下,包含remoteModuleConfig节点。节点信息包括:moduleName, methodId, methodName, args。
var BatchedBridge = new MessageQueue(function () { return global.__fbBatchedBridgeConfig;});
{ "remoteModuleConfig": { "FrescoModule": { "moduleID": 0, "supportsWebWorkers": false, "methods": {} }, "RNIntentModule": { "moduleID": 1, "supportsWebWorkers": false, "methods": { "openThirdReactActivity": { "methodID": 0, "type": "remote" }, "openSecondReactActivity": { "methodID": 1, "type": "remote" }, "getDataFromIntent": { "methodID": 2, "type": "remote" }, "finishActivity": { "methodID": 3, "type": "remote" }, "backActivity": { "methodID": 4, "type": "remote" }, "openSecondActivity": { "methodID": 5, "type": "remote" } } } }
define(60 /* NativeModules */, function (global, require, module, exports) { 'use strict'; var BatchedBridge = require(61 /* BatchedBridge */); var RemoteModules = BatchedBridge.RemoteModules; function normalizePrefix(moduleName) { return moduleName.replace(/^(RCT|RK)/, ''); } Object.keys(RemoteModules).forEach(function (moduleName) { var strippedName = normalizePrefix(moduleName); if (RemoteModules['RCT' + strippedName] && RemoteModules['RK' + strippedName]) { throw new Error( 'Module cannot be registered as both RCT and RK: ' + moduleName); } if (strippedName !== moduleName) { RemoteModules[strippedName] = RemoteModules[moduleName]; delete RemoteModules[moduleName]; } }); var NativeModules = {}; Object.keys(RemoteModules).forEach(function (moduleName) { Object.defineProperty(NativeModules, moduleName, { configurable: true, enumerable: true, get: function get() { var module = RemoteModules[moduleName]; if (module && typeof module.moduleID === 'number' && global.nativeRequireModuleConfig) { var json = global.nativeRequireModuleConfig(moduleName); var config = json && JSON.parse(json); module = config && BatchedBridge.processModuleConfig(config, module.moduleID); RemoteModules[moduleName] = module; } Object.defineProperty(NativeModules, moduleName, { configurable: true, enumerable: true, value: module }); return module; } }); }); module.exports = NativeModules; }, "NativeModules");
function _genModules(remoteModules) { var _this5 = this; var modules = {}; remoteModules.forEach(function (config, moduleID) { var info = _this5._genModule(config, moduleID); if (info) { modules[info.name] = info.module; } }); return modules;}
function _genModule(config, moduleID) { module[methodName] = _this6._genMethod(moduleID, methodID, methodType); return { name: moduleName, module: module };}
function _genMethod 调用 (module, method, type) { var fn = null; var self = this; if (type === MethodTypes.remoteAsync) { fn = function fn() { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return new Promise(function (resolve, reject) { self.__nativeCall( module, method, args, function (data) { resolve(data); }, function (errorData) { var error = createErrorFromErrorData(errorData); reject(error); }); }); }; } else if (type === MethodTypes.syncHook) { return function () { for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return global.nativeCallSyncHook(module, method, args); }; } else { fn = function fn() { for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { args[_key3] = arguments[_key3]; } var lastArg = args.length > 0 ? args[args.length - 1] : null; var secondLastArg = args.length > 1 ? args[args.length - 2] : null; var hasSuccCB = typeof lastArg === 'function'; var hasErrorCB = typeof secondLastArg === 'function'; hasErrorCB && invariant( hasSuccCB, 'Cannot have a non-function arg after a function arg.'); var numCBs = hasSuccCB + hasErrorCB; var onSucc = hasSuccCB ? lastArg : null; var onFail = hasErrorCB ? secondLastArg : null; args = args.slice(0, args.length - numCBs); return self.__nativeCall(module, method, args, onFail, onSucc); }; } fn.type = type; return fn;}
function __nativeCall(module, method, params, onFail, onSucc) { this._queue[MODULE_IDS].push(module); this._queue[METHOD_IDS].push(method); this._queue[PARAMS].push(params); var now = new Date().getTime(); if (global.nativeFlushQueueImmediate && now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS) { global.nativeFlushQueueImmediate(this._queue); this._queue = [[], [], [], this._callID]; this._lastFlush = now; }}
m_context = JSGlobalContextCreateInGroup(nullptr, nullptr);s_globalContextRefToJSCExecutor[m_context] = this;installGlobalFunction(m_context, "nativeFlushQueueImmediate", nativeFlushQueueImmediate);installGlobalFunction(m_context, "nativePerformanceNow", nativePerformanceNow);installGlobalFunction(m_context, "nativeStartWorker", nativeStartWorker);installGlobalFunction(m_context, "nativePostMessageToWorker", nativePostMessageToWorker);installGlobalFunction(m_context, "nativeTerminateWorker", nativeTerminateWorker);installGlobalFunction(m_context, "nativeInjectHMRUpdate", nativeInjectHMRUpdate);
JSValueRef JSCExecutor::nativeFlushQueueImmediate( JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception) {std::string resStr = Value(ctx, arguments[0]).toJSONString();executor->flushQueueImmediate(resStr);return JSValueMakeUndefined(ctx);
void JSCExecutor::flushQueueImmediate(std::string queueJSON) { m_bridge->callNativeModules(*this, queueJSON, false);}
class BridgeCallback {public: virtual ~BridgeCallback() {}; virtual void onCallNativeModules( ExecutorToken executorToken, const std::string& callJSON, bool isEndOfBatch) = 0; virtual void onExecutorUnregistered(ExecutorToken executorToken) = 0;};void Bridge::callNativeModules(JSExecutor& executor, const std::string& callJSON, bool isEndOfBatch) { m_callback->onCallNativeModules(getTokenForExecutor(executor), callJSON, isEndOfBatch);}BridgeCallback::m_callback 为OnLoad.cpp 中的 class PlatformBridgeCallback : public BridgeCallbackvirtual void onCallNativeModules( ExecutorToken executorToken, const std::string& callJSON, bool isEndOfBatch) override { executeCallbackOnCallbackQueueThread([executorToken, callJSON, isEndOfBatch] (ResolvedWeakReference& callback) { JNIEnv* env = Environment::current(); for (auto& call : react::parseMethodCalls(callJSON)) { makeJavaCall(env, executorToken, callback, call); if (env->ExceptionCheck()) { return; } } if (isEndOfBatch) { signalBatchComplete(env, callback); } });}
相当于执行 PlatformBridgeCallback.onCallNativeModules,最后调用 makeJavaCall方法调用java方法
OnLoad.cpp 中 makeJavaCall 定义, c++通过CallVoidMethod调用java非静态方法:
static void makeJavaCall(JNIEnv* env, ExecutorToken executorToken, jobject callback, const MethodCall& call) { auto newArray = ReadableNativeArray::newObjectCxxArgs(std::move(call.arguments)); env->CallVoidMethod( callback, gCallbackMethod, static_cast<JExecutorTokenHolder*>(executorToken.getPlatformExecutorToken().get())->getJobj(), call.moduleId, call.methodId, newArray.get());}
从编写自定义插件中,我们知道了JS如何调用Native方法,但 @ReactMethod 注解的方法的返回值只能是void,现在JS端想从Native获取一些配置信息或者知道调用端是否成功的一些返回值信息,该如何实现呢?
JavaScript调用Native获取Native返回值是通过异步Callback实现的. 在JS调用Native时,会判断方法的最后两个参数,如果是function,就会把函数放到callback数值中, key为自增的callbackId,同时把callbackId传递给Native。Native执行完以后,通过调用JS方法 __invokeCallback 进行回调。在react-native中定是通过Callback和Promise的接口,用来处理JavaScript调用Java方法的回调,Callback会作为ReactMethod注解方法的一个参数,Native调用JS就是通过这个Callback实现的,具体实现会在下面讲到。
首先,我们看一下Callback和Promise具体实现,然后根据代码来剖析实现原理。
@ReactMethodpublic void setCache(String key, String value, Callback successCallback, Callback errorCallback) { try { sharedPreference = getCurrentActivity().getSharedPreferences("rn_cache", 0); sharedPreference.edit().putString(key, value).commit(); successCallback.invoke("save success"); } catch (Exception e) { e.printStackTrace(); errorCallback.invoke(e.getMessage()); }}@ReactMethodpublic void getCache(String key, Callback callback) { callback.invoke(sharedPreference.getString(key, ""));}
_setCacheClick(){ NativeModules.IntentPackage.setCache('ReactNative','我是来自React Native缓存消息',(msg)=>{ NativeModules.ToastAndroid.show(msg, 3000); },(error)=>{ NativeModules.ToastAndroid.show(error, 3000); });}_getCacheClick(){ NativeModules.IntentPackage.getCache('ReactNative',(value)=>{ NativeModules.ToastAndroid.show(value, 3000) });}
@ReactMethodpublic void setCachePromise(String key, String value, Promise promise) { try { sharedPreference = getCurrentActivity().getSharedPreferences("rn_cache", 0); sharedPreference.edit().putString(key, value).commit(); promise.resolve("save success"); } catch (Exception e) { e.printStackTrace(); promise.resolve(e.getMessage()); }}@ReactMethodpublic void getCachePromise(String key, Promise promise) { promise.resolve(sharedPreference.getString(key, ""));}
_setCachePromiseClick(){ NativeModules.IntentPackage.setCache('ReactNative','我是来自React Native缓存消息').then(msg=>{ NativeModules.ToastAndroid.show(msg, 3000); },error=>{ NativeModules.ToastAndroid.show(error, 3000); }).}_getCachePromiseClick(){ NativeModules.IntentPackage.getCache('ReactNative').then(function(value){ NativeModules.ToastAndroid.show(value, 3000) });}
private ReactBridge initializeBridge(JavaScriptExecutor jsExecutor) { bridge = new ReactBridge( jsExecutor, new NativeModulesReactCallback(), mReactQueueConfiguration.getNativeModulesQueueThread()); return bridge; }public ReactBridge( JavaScriptExecutor jsExecutor, ReactCallback callback, MessageQueueThread nativeModulesQueueThread) { mJSExecutor = jsExecutor; mCallback = callback; mNativeModulesQueueThread = nativeModulesQueueThread; initialize(jsExecutor, callback, mNativeModulesQueueThread);}
@DoNotStrippublic interface ReactCallback { @DoNotStrip void call(ExecutorToken executorToken, int moduleId, int methodId, ReadableNativeArray parameters);}private class NativeModulesReactCallback implements ReactCallback { @Override public void call(ExecutorToken executorToken, int moduleId, int methodId, ReadableNativeArray parameters) { synchronized (mJSToJavaCallsTeardownLock) { // NativeModuleRegistry调用call nativeModuleRegistry.call(CatalystInstanceImpl.this, executorToken, moduleId, methodId, parameters); } }}
public class NativeModuleRegistry { private static class MethodRegistration { public MethodRegistration(String name, String tracingName, NativeModule.NativeMethod method) { this.name = name; this.tracingName = tracingName; this.method = method; } public String name; public String tracingName; // Native 模块必须实现的接口NativeModule public NativeModule.NativeMethod method; } } private static class ModuleDefinition { public final int id; public final String name; public final NativeModule target; public final ArrayList<MethodRegistration> methods; public ModuleDefinition(int id, String name, NativeModule target) { this.id = id; this.name = name; this.target = target; this.methods = new ArrayList<MethodRegistration>(); // target.getMethods() 收集 @ReactMehtod 注解的方法 for (Map.Entry<String, NativeModule.NativeMethod> entry : target.getMethods().entrySet()) { this.methods.add( new MethodRegistration( entry.getKey(), "NativeCall__" + target.getName() + "_" + entry.getKey(), entry.getValue())); } } public void call( CatalystInstance catalystInstance, ExecutorToken executorToken, int methodId, ReadableNativeArray parameters) { // this.methods.get(methodId).method == NativeModule.NativeMethod this.methods.get(methodId).method.invoke(catalystInstance, executorToken, parameters); } }}
public abstract class BaseJavaModule implements NativeModule { static final private ArgumentExtractor<Callback> ARGUMENT_EXTRACTOR_CALLBACK = new ArgumentExtractor<Callback>() { @Override public @Nullable Callback extractArgument( CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { if (jsArguments.isNull(atIndex)) { return null; } else { int id = (int) jsArguments.getDouble(atIndex); //CallbackImpl 实现 Callback接口 return new CallbackImpl(catalystInstance, executorToken, id); } } }; static final private ArgumentExtractor<Promise> ARGUMENT_EXTRACTOR_PROMISE = new ArgumentExtractor<Promise>() { @Override public int getJSArgumentsNeeded() { return 2; } @Override public Promise extractArgument( CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray jsArguments, int atIndex) { Callback resolve = ARGUMENT_EXTRACTOR_CALLBACK .extractArgument(catalystInstance, executorToken, jsArguments, atIndex); Callback reject = ARGUMENT_EXTRACTOR_CALLBACK .extractArgument(catalystInstance, executorToken, jsArguments, atIndex + 1); return new PromiseImpl(resolve, reject); } }; private @Nullable Map<String, NativeMethod> mMethods; private @Nullable Map<String, SyncNativeHook> mHooks; // getMethods实现 @Override public final Map<String, NativeMethod> getMethods() { Method[] targetMethods = getClass().getDeclaredMethods(); for (Method targetMethod : targetMethods) { if (targetMethod.getAnnotation(ReactMethod.class) != null) { String methodName = targetMethod.getName(); mMethods.put(methodName, new JavaMethod(targetMethod)); } if (targetMethod.getAnnotation(ReactSyncHook.class) != null) { String methodName = targetMethod.getName(); mHooks.put(methodName, new SyncJavaHook(targetMethod)); } } return assertNotNull(mMethods); } public class JavaMethod implements NativeMethod { private Method mMethod; public JavaMethod(Method method) { mMethod = method; } @Override public void invoke(CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray parameters) { mMethod.invoke(BaseJavaModule.this, mArguments); } }}public abstract class ReactContextBaseJavaModule extends BaseJavaModule {}public class IntentModule extends ReactContextBaseJavaModule {}
private native void initialize( JavaScriptExecutor jsExecutor, ReactCallback callback, MessageQueueThread nativeModulesQueueThread);public final class CallbackImpl implements Callback { private final CatalystInstance mCatalystInstance; private final ExecutorToken mExecutorToken; private final int mCallbackId; public CallbackImpl(CatalystInstancebridge = new ReactBridge( jsExecutor, new NativeModulesReactCallback(), mReactQueueConfiguration.getNativeModulesQueueThread()); bridge, ExecutorToken executorToken, int callbackId) { mCatalystInstance = bridge; mExecutorToken = executorToken; mCallbackId = callbackId; } @Override public void invoke(Object... args) { mCatalystInstance.invokeCallback(mExecutorToken, mCallbackId, Arguments.fromJavaArgs(args)); }}
@Overridepublic void invokeCallback(ExecutorToken executorToken, int callbackID, NativeArray arguments) {if (mIsBeingDestroyed) { FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed."); return;}synchronized (mJavaToJSCallsTeardownLock) { if (mDestroyed) { FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed."); return; } incrementPendingJSCalls(); Assertions.assertNotNull(mBridge).invokeCallback(executorToken, callbackID, arguments);}}
public native void invokeCallback(ExecutorToken executorToken, int callbackID, NativeArray arguments);
static void invokeCallback(JNIEnv* env, jobject obj, JExecutorToken::jhybridobject jExecutorToken, jint callbackId, NativeArray::jhybridobject args) { auto bridge = extractRefPtr<CountableBridge>(env, obj); auto arguments = cthis(wrap_alias(args)); try { bridge->invokeCallback( cthis(wrap_alias(jExecutorToken))->getExecutorToken(wrap_alias(jExecutorToken)), (double) callbackId, std::move(arguments->array) ); } catch (...) { translatePendingCppExceptionToJavaException(); }}
void Bridge::invokeCallback(ExecutorToken executorToken, const double callbackId, const folly::dynamic& arguments) { #ifdef WITH_FBSYSTRACE int systraceCookie = m_systraceCookie++; FbSystraceAsyncFlow::begin( TRACE_TAG_REACT_CXX_BRIDGE, "<callback>", systraceCookie); #endif #ifdef WITH_FBSYSTRACE runOnExecutorQueue(executorToken, [callbackId, arguments, systraceCookie] (JSExecutor* executor) { FbSystraceAsyncFlow::end( TRACE_TAG_REACT_CXX_BRIDGE, "<callback>", systraceCookie); FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "Bridge.invokeCallback"); #else runOnExecutorQueue(executorToken, [callbackId, arguments] (JSExecutor* executor) { #endif executor->invokeCallback(callbackId, arguments); });}
void JSCExecutor::invokeCallback(const double callbackId, const folly::dynamic& arguments) { String argsString = String(folly::toJson(std::move(arguments)).c_str()); JSValueRef args[] = { JSValueMakeNumber(m_context, callbackId), Value::fromJSON(m_context, argsString) }; // m_invokeCallbackObj = // folly::make_unique<Object>( // m_batchedBridge->getProperty("invokeCallbackAndReturnFlushedQueue").asObject()); // 执行回调,返回待执行的队列 auto result = m_invokeCallbackObj->callAsFunction(2, args); // 调用java方法 m_bridge->callNativeModules(*this, result.toJSONString(), true);}
function invokeCallbackAndReturnFlushedQueue(cbID, args) { var _this3 = this; guard(function () { // 执行回调 _this3.__invokeCallback(cbID, args); _this3.__callImmediates(); }); // 返回JS调用Native的队列 return this.flushedQueue();}
]]>在用 Node.js + Webpack 构建的方式进行开发时, 我们希望能实现修改代码能实时刷新页面UI的效果. 这个特性 Webpack本身是支持的, 而且基于koa也有现成的 koa-webpack-hot-middleware 和 koa-webpack-dev-middleware 封装好的组件支持.
不过这里如果需要支持Node.js服务器端修改代码自动重启webpack自动编译功能该如何实现呢, 主要存在以下几个问题:
前端渲染和服务端渲染构建热更新实现
在koa项目中, 通过 koa-webpack-dev-middleware 和 koa-webpack-hot-middleware 可以实现 Webpack 编译内存存储和热更新功能, 代码如下:
const compiler = webpack(webpackConfig);const devMiddleware = require('koa-webpack-dev-middleware')(compiler, options);const hotMiddleware = require('koa-webpack-hot-middleware')(compiler, options);app.use(devMiddleware);app.use(hotMiddleware);
如果按照上面实现, 可以满足修改修改客户端代码实现webpack自动变编译和UI界面热更新的功能.
但如果是修改 Node.js 服务器端代码重启后就会发现webpack会重新编译, 这不是我们要的效果.
原因是因为 middleware 是依赖 app 的生命周期, 当 app 销毁时, 对应 Webpack compiler 实例也就没有了, 重启时会重新执行middleware 初始化工作.
那有没有办法保持 Webpack 编译实例呢? 针对这个我们可以通过 Egg 框架已经内置了 worke r和 agent 机制来实现 Webpack内存编译功能.
node index.js
或者 npm run dev
启动 Egg 应用app.vue.renderBundle = (name, context, options) => { const filePath = path.isAbsolute(name) ? name : path.join(app.config.view.root[0], name); const promise = app.webpack.fileSystem.readWebpackMemoryFile(filePath, name); return co(function* () { const content = yield promise; if (!content) { throw new Error(`read webpack memory file[${filePath}] content is empty, please check if the file exists`); } return renderBundle.bind(app.vue)(content, context, options); });};
浏览器输入URL请求地址, 然后Egg接收到请求, 然后进入Controller
Node层获取数据后(Node通过http/rpc方式调用Java后端API数据接口), 进入模板render流程
进入render流程后, 通过worker进程通过调用 app.messenger.sendToAgent
发送文件名给Agent进程, 同时通过 app.messenger.on
启动监听监听agent发送过来的消
Agent进程获取到文件名后, 从Webpack编译内存里面获取文件内容, 然后Agent 通过 agent.messenger.sendToApp
把文件内容发送给Worker进程
Worker进程获取到内容以后, 进行Vue编译HTML, 编译成HTML后, 进入jss/css资源依赖流程
如果启动代理模式(见easywebpack的setProxy), HTML直接注入相对路径的JS/CSS, 如下:
页面可以直接使用 /public/client/js/vendor.js
相对路径, /public/client/js/vendor.js
由后端框架代理转发到webpack编译服务, 然后返回内容给后端框架, 这里涉及两个应用通信. 如下:
<link rel="stylesheet" href="/public/client/css/home/android/home.css">
如果非代理模式, HTML直接注入必须是绝对路径的 JS/CSS , 如下:
页面必须使用
http://127.0.0.1:9001/public/client/js/vendor.js
绝对路径
其中 http://127.0.0.1:9001 是 Agent里面启动的Webpack编译服务地址, 与Egg应用地址是两回事
最后, 模板渲染完成, 服务器输出HTML内容给浏览器.
easy build prod
manfifest.json
文件注入 jss/css 资源依赖注入.npm run dev
后你会看到如下界面, 启动了两个 Webpack 构建实例:Node 模式 和 Web 模式。SSR 运行需要 Webapck 单独构建 target: node
和 target: web
主要的差异在于 Webpack需要处理 require 机制以及磨平 Node 和 浏览器运行环境的差异。在 easywebpack
4.6.0 以下 SSR 版本构建方案实现时,Node 和 Web 模式采用的是一份 .babelrc
配置,这样导致构建的后代码全部变成 es5。 但 Node 现在LTS 版本已经是 8 了,而且 10 也在开发,不久将会发布。这样导致 Node 端构建的代码没有用到 ES6 的特性,我们期望根据 Node 版本构建指定 ES 模式代码,这样可以带来两个好处:
.babelrc
配置{ "presets": [["env",{ "modules": false }]], "plugins": [ "transform-object-rest-spread", "syntax-dynamic-import", "transform-object-assign" ], "comments": false}
注意: 升级 babel 7 后,不支持如下 env 方式配置
关键措施: bable 本身支持通过 process.env.BABEL_ENV 加载 .babelrc 配置文件:
如果.babelrc 配置了 env.node
或者 env.web
节点配置,easywebpack 底层将自动设置 **process.env.BABEL_ENV 变量, 启动 BABEL ENV 编译机制。easywebpack 底层支持 process.env.BABEL_ENV 支持 node 和 web 的 env .babelrc 节点配置。 另外关键的 target 配置:**
target.node
: Node 环境编译模式,可以是指定版本,比如配置:8.9.3,也可以配置当前运行的node版本:current。target.browsers
: Web 浏览器编译模式,可以配置浏览器的版本等{ "env":{ "node": { "presets": [["env", { "modules": false, "targets": { "node": "current" } }]], "plugins": [ "transform-object-rest-spread", "syntax-dynamic-import" ] }, "web": { "presets": [["env", { "modules": false, "targets": { "browsers": ["last 2 versions", "safari >= 7"] } }]], "plugins": [ "transform-object-rest-spread", "syntax-dynamic-import", "transform-object-assign" ] } }, "comments":false}
合理的使用 BABEL 编译模式,能够极大提高构建速度和JS 文件大小。 通过测试,启用 BABEL_ENV 模式和合理的配置 targets.browsers 参数,对于大型的页面,能够显著提升构建速度。下面通过 easy build prod
针对 https://github.com/hubcarl/egg-vue-webpack-boilerplate 测试的效果,页面比较简单,效果不明显。
模式 | 构建大小(app/app.js) |
---|---|
不启用BABEL按需编译 | 15.6 KiB |
启用BABEL按需编译 | 15.2 KiB |
原文:https://hubcarl.github.io/blog/2015/04/11/hybrid-cordova/
CordovaActivity:Cordova Activity入口,已实现PluginManager、WebView的相关初始化工作, 只需继承CordovaActivity实现自己的业务需求。
PluginManager: 插件管理器
ExposedJsApi :javascript调用Native, 通过插件管理器PluginManager 根据service找到具体实现类。
NativeToJsMessageQueue:Native调用javascript,主要包括三种方式:loadUrl 、 轮询、反射WebViewCore执行js.
当实现了DroidGap或者CordovaInterface接口的Activity的onCreate方法中调用DroidGap的loadUrl方法即启动了Cordova框架。
Cordova提供了一个Class(DroidGap extends CordovaActivity)和一个interface(CordovaInterface)来让Android开发者开发Cordova。
一般情况下实现DroidGap即可,因为DroidGap类已经做了很多准备工作,可以说DroidGap类是Cordova框架的一个重要部分;如果在必要的情况下实现CordovaInterface接口,那么这个类中很多DroidGap的功能需要自己去实现。继承了DroidGap或者CordovaInterface的Activity就是一个独立的Cordova模块,独立的Cordova模块指的是每个实现了DroidGap或者CordovaInterface接口的Activity都对应一套独立的WebView,Plugin,PluginManager,没有共享的。
在初始化完CordovaWebView后调用CordovaWebView.loadUrl()。此时完成Cordova的启动。
在实例化CordovaWebView的时候, CordovaWebView对象会去创建一个属于当前CordovaWebView对象的插件管理器PluginManager对象,一个消息队列NativeToJsMessageQueue对象,一个JavascriptInterface对象ExposedJsApi,并将ExposedJsApi对象添加到CordovaWebView中,JavascriptInterface名字为:_cordovaNative。
在创建ExposedJsApi时需要CordovaWebView的PluginManager对象和NativeToJsMessageQueue对象。因为所有的JS端与Android native代码交互都是通过ExposedJsApi对象的exec方法。在exec方法中执行PluginManager的exec方法,PluginManager去查找具体的Plugin并实例化然后再执行Plugin的execute方法,并根据同步标识判断是同步返回给JS消息还是异步。由NativeToJsMessageQueue统一管理返回给JS的消息。
Cordova在启动每个Activity的时候都会将配置文件中的所有plugin加载到PluginManager。那么是什么时候将这些plugin加载到PluginManager的呢?在b中说了最后会调用CordovaWebView.loadUrl(),对,就在这个时候会去初始化PluginManager并加载plugin。PluginManager在加载plugin的时候并不是马上实例化plugin对象,而是只是将plugin的Class名字保存到一个hashmap中,用service名字作为key值。
当JS端通过JavascriptInterface接口的ExposedJsApi对象请求Android时,PluginManager会从hashmap中查找到plugin,如果该plugin还未实例化,利用java反射机制实例化该plugin,并执行plugin的execute方法。
Cordova中通过exec()函数请求android插件,数据的返回可同步也可以异步于exec()函数的请求。在开发android插件的时候可以重写public boolean isSynch(String action)方法来决定是同步还是异步。Cordova在android端使用了一个队列(NativeToJsMessageQueue)来专门管理返回给JS的数据。
1)同步
Cordova在执行完exec()后,android会马上返回数据,但不一定就是该次请求的数据,可能是前面某次请求的数据;因为当exec()请求的插件是允许同步返回数据的情况下,Cordova也是从NativeToJsMessageQueue队列头pop头数据并返回。然后再根据callbackID反向查找某个JS请求,并将数据返回给该请求的success函数。
2)异步
Cordova在执行完exec()后并不会同步得到一个返回数据。Cordova在执行exec()的同时启动了一个XMLHttpRequest对象方式或者prompt()函数方式的循环函数来不停的去获取NativeToJsMessageQueue队列中的数据,并根据callbackID反向查找到相对应的JS请求,并将该数据交给success函数。
Cordova对本地的HTML文件(file:// 开头的URL)或者手机设置有代理的情况下使用XMLHttpRequest方式获取返回数据,其他则使用prompt()函数方式获取返回数据。
Native 调用 JS 执行方式有三种实现 LoadUrlBridgeMode、 OnlineEventsBridgeMode、PrivateApiBridgeMode
如果是gap_bridge_mode,则执行 appView.exposedJsApi.setNativeToJsBridgeMode(Integer.parseInt(message));
如果是gap_poll, 则执行 appView.exposedJsApi.retrieveJsMessages(“1”.equals(message));
private class LoadUrlBridgeMode extends BridgeModeif (url.startsWith("file://") || url.startsWith("javascript:") || Config.isUrlWhiteListed(url)) {}
private class OnlineEventsBridgeMode extends BridgeMode
—可以解决loadUrl 隐藏键盘的问题:当你的焦点在输入,如果这通过loadUrl调用js,会导致键盘隐藏
private class PrivateApiBridgeMode extends BridgeModeField f = webViewClass.getDeclaredField("mProvider");f.setAccessible(true);webViewObject = f.get(webView);webViewClass = webViewObject.getClass();Field f = webViewClass.getDeclaredField("mWebViewCore");f.setAccessible(true);webViewCore = f.get(webViewObject);if (webViewCore != null) { sendMessageMethod = webViewCore.getClass().getDeclaredMethod("sendMessage", Message.class); sendMessageMethod.setAccessible(true); } Message execJsMessage = Message.obtain(null, EXECUTE_JS, url); sendMessageMethod.invoke(webViewCore, execJsMessage);
boolean isHoneycomb = (SDK_INT >= Build.VERSION_CODES.HONEYCOMB && SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR2);// Bug being that Java Strings do not get converted to JS strings automatically.// This isn't hard to work-around on the JS side, but it's easier to just use the prompt bridge instead.if (isHoneycomb || (SDK_INT < Build.VERSION_CODES.GINGERBREAD)) {Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old.");return;} else if (SDK_INT < Build.VERSION_CODES.HONEYCOMB && Build.MANUFACTURER.equals("unknown")) {// addJavascriptInterface crashes on the 2.3 emulator.Log.i(TAG, "Disabled addJavascriptInterface() bridge callback due to a bug on the 2.3 emulator");return;}this.addJavascriptInterface(exposedJsApi, "_cordovaNative");
]]>原文: https://hubcarl.github.io/blog/2017/09/23/git/
git config credential.helper store
git branch | grep -E feature\/(1\.|activity|btn_s|refresh|share|3\.1)|xargs git branch -Dgit branch | grep -E develop\/(test|script_inline|egg3|3\.1)|xargs git branch -Dgit branch | grep -E release\/|xargs git branch -D// 删除除master的本地分支git branch | grep -v 'master'|xargs git branch -D// -v 取反git branch | grep -v 'master\|feature\/benchmark\|feature\/async-component'// 取反删除git branch | grep -v 'master\|feature\/benchmark\|feature\/async-component'|xargs git branch -D
// 查找取反显示git branch -r| grep -v 'master\|feature\/benchmark\|feature\/async-component'git branch -r | grep -v 'master\|feature\/benchmark\|feature\/async-component' | awk '{print $1}'// 查找取反git branch -r| grep -v 'master\|feature\/benchmark\|feature\/async-component\|develop\/stearmrender'// 查找git branch -r| grep -E 'master|feature\/benchmark|feature\/async-component|develop\/stearmrender'// 筛选远程分支git branch -r| awk -F '[/]' '/release|hotfix/ {printf "%s/%s\n",$2,$3}' // 删除远程分支git branch -r| awk -F '[/]' '/release|hotfix/ {printf "%s/%s\n",$2,$3}'|xargs -i {} git push origin :{}git branch -r |awk -F '[/]' '/(master|feature\/benchmark|feature\/async-component)/{printf "%s/%s/%s\n", $2,$3,$4}' git branch -r |awk -F '[/]' '/(master|feature\/benchmark|feature\/async-component)/{printf "%s/%s/%s\n", $2,$3,$4}' |xargs -I {} git push origin :{}// 终极取反筛选查找git branch -r| grep -v 'master\|feature\/benchmark\|feature\/develop\/stearmrender'|awk -F '[/]' '/\// {printf "%s/%s\n", $2,$3}'// 终极取反筛选查找删除git branch -r| grep -v 'master\|feature\/benchmark\|feature\/|develop\/stearmrender'|awk -F '[/]' '/\// {printf "%s/%s\n", $2,$3}' |xargs -I {} git push origin :{}// 分支格式: feature/test, 删除远程除master的分支git branch -r| grep -v 'master'|awk -F '[/]' '/\// {printf "%s/%s\n", $2,$3}' |xargs -I {} git push origin :{}// 分支格式: feature/test/test, 删除远程除master的分支git branch -r| grep -v 'master'|awk -F '[/]' '/\// {printf "%s/%s/%s\n", $2,$3,$4}' | xargs -I {} git push origin :{}// 分支格式: feature/test/test/test, 删除远程除master的分支git branch -r| grep -v 'master'|awk -F '[/]' '/\// {printf "%s/%s/%s/%s\n", $2,$3,$4,$5}' |xargs -I {} git push origin :{}// 运行git fetch -p 同步最新远程分支
curl -Lo /dev/null -skw “time_connect: %{time_connect}
s\ntime_namelookup: %{time_namelookup}
s\ntime_pretransfer: %{time_pretransfer}
s\ntime_starttransfer: %{time_starttransfer}
s\ntime_redirect: %{time_redirect}
s\nspeed_download: %{speed_download} B/s\ntime_total: %{time_total} s\n\n”
(1)type
提交 commit 的类型,包括以下几种
feat: 新功能
fix: 修复问题
docs: 修改文档
style: 修改代码格式,不影响代码逻辑
refactor: 重构代码,理论上不影响现有功能
perf: 提升性能
test: 增加修改测试用例
chore: 修改工具相关(包括但不限于文档、代码生成等)
deps: 升级依赖
(2)scope
修改文件的范围(包括但不限于 doc, middleware, proxy, core, config, plugin)
(3)subject
用一句话清楚的描述这次提交做了什么
brew install git-extrasgit-extras 命令生成 changelog 和 release 自动打 tag & push & trigger hook$ git changelog # 需要修改 Histroy.md 和 package.json 的版本号,如需要发布 1.0.0 $ git release 1.0.0提交规范实例:git commit -m 'fix($guild): solve bugs'
{ "scripts": { "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", },}
递增一个修订号 npm version patch
递增一个次版本号 npm version minor
递增一个主版本号 npm version major
合并commit message
git rebase -i HEAD~3
pick
edit
squash
https://git-scm.com/book/zh/v1/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E5%86%99%E5%8E%86%E5%8F%B2
修复问题发布一个npm版本
checkout new branch
修改代码,提交 git commit -m ‘fix($guild): solve bugs’
git changelog 需要修改 Histroy.md 和 package.json 的版本号,如需要发布 1.0.0
npm version patch 升级一个小版本
git release 1.0.0 (自动打 tag & push & trigger hook)
npm publish 发布一个版本
在 前端渲染模式 和 asset 渲染模式 章节讲到了基于 React 的前端渲染模式,但都依赖 egg-view-react-ssr 插件,那如何基于已有 egg 模板引擎 (egg-view-nunjucks 或 egg-view-ejs) + Webpack 完全自定义前端方案呢?
html-webpack-plugin
插件生成 HTML 文件,并自动注入 JS/CSS 依赖write-file-webpack-plugin
插件把 Webpack HTML 文件写到本地。Webpack 默认是在内存里面,无法直接读取。这里以 egg-view-nunjucks 为例,其它模板引擎类似。
npm install egg-view-nunjucks --save
npm install egg-webpack --save-dev
// ${root}/package.json{ "dependencies": { "egg-webpack": "^4.0.0", "egg-view-nunjucks": "^2.2.0", }}
// ${root}/config/plugin.local.jsexports.nunjucks = { enable: true, package: 'egg-webpack',};// ${root}/config/plugin.jsexports.nunjucks = { enable: true, package: 'egg-view-nunjucks',};
<!DOCTYPE html><html lang='en'><head> <title>{{title}}</title> <meta name='keywords'> <meta name='description'> <meta http-equiv='content-type' content='text/html;charset=utf-8'> <meta name='viewport' content='initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui'> <link rel='shortcut icon' href='/favicon.ico' type='image/x-icon' /> <!-- html-webpack-plugin 自动注入 css --></head><body> <div id='app'></div> <script type="text/javascript"> window.__INITIAL_STATE__ = {{ }}; </script> <!-- html-webpack-plugin 自动注入 js --></body></html>
// ${root}/config/local.jsmodule.exports = app => { const exports = {}; exports.webpack = { webpackConfigList: require('easywebpack-react').getWebpackConfig() }; return exports;}// ${root}/config/default.jsmodule.exports = app => { const exports = {}; exports.view = { defaultViewEngine: 'nunjucks', mapping: { '.tpl': 'nunjucks' }, }; return exports;}
const egg = require('egg');module.exports = class AppController extends egg.Controller { async home(ctx) { await ctx.render('layout.tpl', { title: '自定义渲染' }); }}
该配置基于 easywebpack 配置,如果要用原生 webpack 请参考:/blog/wumyiw
const HtmlWebpackPlugin = require('html-webpack-plugin');const WriteFilePlugin = require('write-file-webpack-plugin');module.exports = { egg: true, target: 'web', entry: { app: 'app/web/page/app/app.js' }, plugins: [ new HtmlWebpackPlugin({ filename: '../app/view/layout.tpl', template: './app/web/view/layout.tpl' }), new WriteFilePlugin({ test: /\.tpl$/ }) ]};
https://github.com/easy-team/egg-react-webpack-boilerplate/tree/feature/green/html
]]>原文:https://hubcarl.github.io/blog/2015/04/19/hybrid-phonegap/
首先, 来看一下phonegap 初始化流程以及Native 与 JS 交互流程图。
说明:socket server模式下, phonegap.js 源码实现的采用1 毫秒执行一次XHR请求, 当Native JS 队列里面有JS语句数据时,才是真正的1毫秒调用一下; 当没有数据, scoket server 会阻塞10毫秒, 也就是XHR 要等10秒钟才能收到结果,并进行下一次的轮询。
从phonegap.xml 中加载白名单配置 和 log配置
》》初始化webview
》》初始化callbackServer
》》插件管理器PluginManager
》》读取 plugins.xml 配置,用map存储起来。
<plugins><plugin name="Camera" value="com.phonegap.CameraLauncher"/><plugin name="Contacts" value="com.phonegap.ContactManager"/><plugin name="Crypto" value="com.phonegap.CryptoHandler"/><plugin name="File" value="com.phonegap.FileUtils"/><plugin name="Network Status" value="com.phonegap.NetworkManager"/></plugins>
说明:
name 是别名,javascript调用时通过别名来调用。value:java具体实现类web页面调用(例如查找联想人)PhoneGap.exec(successCB, errorCB, "Contacts", "search", [fields, options]);
》》编程java类,继承Plugin类(Plugin 实现了IPlugin接口),并实现execute方法。
例如联系人管理插件:
public class ContactManager extends Plugin{ /** * action : 用来指定一个具体动作 search 表示搜索联系人 * args: 方法参数 * callbackId:js与java指定一个标识, */ public PluginResult execute(String action, JSONArray args, String callbackId) { try { if (action.equals("search")) { JSONArray res = contactAccessor.search(args.getJSONArray(0), args.optJSONObject(1)); return new PluginResult(status, res, "navigator.contacts.cast"); } else if (action.equals("save")) { String id = contactAccessor.save(args.getJSONObject(0)); if (id != null) { JSONObject res = contactAccessor.getContactById(id); if (res != null) { return new PluginResult(status, res); } } } else if (action.equals("remove")) { if (contactAccessor.remove(args.getString(0))) { return new PluginResult(status, result); } } // If we get to this point an error has occurred JSONObject r = new JSONObject(); r.put("code", UNKNOWN_ERROR); return new PluginResult(PluginResult.Status.ERROR, r); } catch (JSONException e) { Log.e(LOG_TAG, e.getMessage(), e); return new PluginResult(PluginResult.Status.JSON_EXCEPTION); } }}
android DroidGap 初始化时,如果loadUrl的url不是以file://开头时,polling = true, 否则是socket server方式
代码见CallbackServer.java 类init方法:
public void init(String url) { if ((url != null) && !url.startsWith("file://")) { this.usePolling = true; this.stopServer(); } else if (android.net.Proxy.getDefaultHost() != null) { this.usePolling = true; this.stopServer(); } else { this.usePolling = false; this.startServer(); }}
phonegap.js在启动时,首先会通过prompt(“usePolling”, “gap_callbackServer:”)获取调用方式:
XHR 轮询 OR prompt 轮询, 如果是XHR的话, 会启动XHR调用获取http server端口 和token。
方法PhoneGap.Channel.join 启动 js server 或者polling调用
UsePolling 默认为false。 通过 var polling = prompt(“usePolling”, “gap_callbackServer:”) 获取调用方式。
PhoneGap.Channel.join(function () { // Start listening for XHR callbacks setTimeout(function () { if (PhoneGap.UsePolling) { PhoneGap.JSCallbackPolling(); } else { console.log('PhoneGap.Channel.join>>>>>>>>>>>>>>>>>>>>>>>>>');<br> <span style="color: #ff6600;"> //phonegap js 首次启动获取js调用Native方式</span> var polling = prompt("usePolling", "gap_callbackServer:"); PhoneGap.UsePolling = polling; if (polling == "true") { PhoneGap.UsePolling = true; <span style="color: #ff6600;">PhoneGap.JSCallbackPolling();</span> } else { PhoneGap.UsePolling = false; <span style="color: #ff6600;"> PhoneGap.JSCallback();</span> } } }, 1);}
XHR轮询:PhoneGap.JSCallback方法
通过XHR 与java端 socket进行通信,每一毫秒执行一次JSCallback,从android socket获取javascript执行结果代码,最后通过eval动态执行javascript
XHR调用, 通过prompt 获取socket端口 和 token(uuid)
if (PhoneGap.JSCallbackPort === null) { PhoneGap.JSCallbackPort = <span style="color: #ff6600;">prompt("getPort", "gap_callbackServer:");</span> console.log('PhoneGap.JSCallback getPort>>>>>>>>>>>>>>>>>>>>>>>>>:' + PhoneGap.JSCallbackPort);}if (PhoneGap.JSCallbackToken === null) { PhoneGap.JSCallbackToken =<span style="color: #ff6600;"> prompt("getToken", "gap_callbackServer:");</span> console.log('PhoneGap.JSCallback getToken>>>>>>>>>>>>>>>>>>>>>>>>>:' + PhoneGap.JSCallbackToken);}xmlhttp.open("GET", "http://127.0.0.1:" + PhoneGap.JSCallbackPort + "/" + PhoneGap.JSCallbackToken, true);xmlhttp.send();
XHR返回结果代码片段
var msg = decodeURIComponent(xmlhttp.responseText);setTimeout(function () {try { var t = eval(msg);}catch (e) { // If we're getting an error here, seeing the message will help in debugging console.log("JSCallback: Message from Server: " + msg); console.log("JSCallback Error: " + e);} }, 1); <span style="color: #ff6600;">setTimeout(PhoneGap.JSCallback, 1);</span><br>}``` prompt轮询: PhoneGap.JSCallbackPolling方法```jsPhoneGap.JSCallbackPolling = function () { // Exit if shutting down app if (PhoneGap.shuttingDown) { return; } // If polling flag was changed, stop using polling from now on if (!PhoneGap.UsePolling) { PhoneGap.JSCallback(); return; } var msg = prompt("", "gap_poll:"); if (msg) { setTimeout(function () { try { var t = eval("" + msg); } catch (e) { console.log("JSCallbackPolling: Message from Server: " + msg); console.log("JSCallbackPolling Error: " + e); } }, 1); <span style="color: #ff6600;">setTimeout(PhoneGap.JSCallbackPolling, 1);</span> } else { setTimeout(PhoneGap.JSCallbackPolling, PhoneGap.JSCallbackPollingPeriod); } };
phonegap 已经改名cordova, 在最新版本cordova 框架里面已经去掉了socket server模式, 详细请查看:http://www.cnblogs.com/hubcarl/p/4202784.html
]]>app.webpack
钩子从内存读取文件内容,解决本地开发 Server Side Render 文件渲染内容读取问题npm install egg-webpack --save
// config/plugin.local.jsexports.webpack = { enable: true, package: 'egg-webpack'};
// config/config.local.jsexports.webpack = { // 这里的 webpack.config.js 为原生 Webpack 配置即可 webpackConfigList: [require('../webpack.config.js')]};
npm install react-hot-loader --save-devnpm install webpack progress-bar-webpack-plugin webpack-manifest-plugin --save-devnpm install @babel/core @babel/preset-env @babel/preset-react babel-loader --save-dev
// ${root}/webpack.config.jsconst path = require('path');const webpack = require('webpack');const ProgressBar = require('progress-bar-webpack-plugin');const ManifestPlugin = require('webpack-manifest-plugin');const isDev = process.env.NODE_ENV !== 'production';module.exports = { mode: process.env.NODE_ENV, entry: isDev ? { app: './app/web/index.js' } : { app:[ 'react-hot-loader/babel', // egg-webpack 默认端口为 9000 'webpack-hot-middleware/client?path=http://127.0.0.1:9000/__webpack_hmr&noInfo=false&reload=false&quiet=false', './app/web/index.js' ] }, resolve: { extensions: [ '.jsx', '.js' ], }, output: { path: path.join(__dirname, 'app/public'), filename: isDev ? '[name].[hash].js' : '[name].js', publicPath: '/public/' }, module: { rules: [ { test: /\.js?$/, use: 'babel-loader', exclude: /node_modules/, }, { test: /\.css$/, use: [ 'style-loader', 'css-loader'], }, { test: /\.(png|jpg|gif|svg)$/, use: [ { loader: 'file-loader', }, ], }, ], }, plugins:[ new webpack.HotModuleReplacementPlugin(), new ProgressBar(), ]};
'use strict';import React, { Component } from 'react'import ReactDOM from 'react-dom'import { AppContainer } from 'react-hot-loader';import './app.css';class App extends Component { render() { return <div className="title"><h1>React App</h1></div> }}ReactDOM.render(module.hot ? <AppContainer><App /></AppContainer> : <App />, document.getElementById('app'));if (module.hot) { module.hot.accept();}
通过 egg-view-nunjucks 模板引擎进行 layout 模板渲染,同时根据 webpack 生成 manifest.json 获取静态资源的实际路径。
// ${root}/config/plugin.jsexports.nunjucks = { enable: true, package: 'egg-view-nunjucks',};// {app_root}/config/config.default.jsexports.view = { defaultViewEngine: 'nunjucks', mapping: { '.tpl': 'nunjucks', },};
<!DOCTYPE html><html lang='en'><head> <title>{{title}}</title> <meta name='keywords'> <meta name='description'> <meta http-equiv='content-type' content='text/html;charset=utf-8'> <meta name='viewport' content='initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui'> <link rel='shortcut icon' href='/favicon.ico' type='image/x-icon' /> {% for item in asset.css %} <link rel="stylesheet" href='{{item}}' /> {% endfor %}</head><body> <div id='app'></div> <script type="text/javascript"> window.__INITIAL_STATE__ = {{ asset.state | safe }}; </script> {% for item in asset.js %} <script type="text/javascript" src="{{item}}"></script> {% endfor %}</body></html>
const egg = require('egg');const manifest = require('../public/manifest.json');module.exports = class AppController extends egg.Controller { async home(ctx) { const js = [manifest['app.js']]; const css = [manifest['app.css']]; await ctx.render('layout.tpl', { title: 'Egg Webpack Hot Reload', state: {}, asset: { js, css } }); }}
最后就可以 npm run dev (egg-bin dev) 一键启动开发,不用分别单独启动前端应用和 Node 应用,同时解决 Node 重启 Webpack 实例丢失导致重新编译问题。
'usestrict';const path = require('path');const egg = require('egg');const vueServerRenderer = require('vue-server-renderer');module.exports = class IndexController extends egg.Controller { async index(ctx) { const { app } = ctx; const filepath = path.join(app.config.view.root[0], 'app.js'); // server render mode, the webpack config target:node const strJSBundle = await app.webpack.fileSystem.readWebpackMemoryFile(filepath); ctx.body = await vueServerRenderer.createBundleRenderer(strJSBundle).renderToString({}); }};
通过实现相同的模板功能,分别针对无缓存和有缓存进行了对比测试. 测试方法,通过ab
压测工具对页面进行测试,Node层收集页面render渲染时间, 然后进行汇总统计分析。
Nunjucks 测试模板
**
<div> <div> <h2>h5GameList</h2> <ul> {% for item in h5GameList.data.list %} <li><a href="{{item.downloadUrl}}">{{item.name}}-{{item.resume}}</a></li> {% endfor %} </ul> </div> <div> <h2>recommendList</h2> {% for item in recommendList.data %} <div> <a href="{{item.downloadUrl}}">{{item.recommendBriefResume}}</a> <p>{{item.recommendResume}}</p> </div> {% endfor %} </div> <div> <h2>bestList</h2> {% for item in bestList.data.list %} <div> <a href="{{item.downloadUrl}}">{{item.resume}}</a> <p>{{item.packageName}}</p> </div> {% endfor %} </div> <div> <h2>bookingList</h2> {% for item in bookingList.data %} <div> <a href="{{item.logoUrl}}">{{item.name}}-{{item.categoryName}}</a> <p>{{item.resume}}</p> </div> {% endfor %} </div></div>
Vue测试模板
**
<template> <div> <div> <h2>h5GameList</h2> <ul> <li v-for="item in h5GameList.data.list" > <a v-bind:href="item.downloadUrl">{{item.name}}-{{item.resume}}</a> </li> </ul> </div> <div> <h2>recommendList</h2> <div v-for="item in recommendList.data" > <a v-bind:href="item.downloadUrl">{{item.recommendBriefResume}}</a> <p>{{item.recommendResume}}</p> </div> </div> <div> <h2>bestList</h2> <div v-for="item in bestList.data.list" > <a v-bind:href="item.downloadUrl">{{item.resume}}</a> <p>{{item.packageName}}</p> </div> </div> <div> <h2>bookingList</h2> <div v-for="item in bookingList.data"> <a v-bind:href="item.logoUrl">{{item.name}}-{{item.categoryName}}</a> <p>{{item.resume}}</p> </div> </div> </div></template>
ab -c 50 -n 1000 http://ip:port/perf/nunjucks/
ab -c 50 -n 1000 http://ip:port/perf/vue/
其中 -n 表示请求数,-c 表示并发数
ab -c 50 -n 5000 http://ip:port/perf/nunjucks/
ab -c 50 -n 5000 http://ip:port/perf/vue/
从上面统计来看可以得出如下结论:
global.num = 100;
const vm = require(‘vm’);
const code = ‘var ret = num * num * num;’;
const sandbox = { num : 1000};
const benchmark = (msg, fun) => {
const start = new Date;
for (let i = 0; i < 10000; i++) {
fun();
}
const end = new Date;
console.log(msg + ‘: ‘ + (end - start) + ‘ms’);
};
const ctx = vm.createContext(sandbox);
// runInThisContext 用于创建一个独立的沙箱运行空间,code内的代码可以访问外部的global对象,但是不能访问其他变量
benchmark(‘vm.runInThisContext’, () => {
vm.runInThisContext(code);
});
// runInContext 创建一个独立的沙箱运行空间,sandBox将做为global的变量传入code内,但不存在global变量
benchmark(‘vm.runInContext’, () => {
vm.runInContext(code, ctx);
});
// 与runInContext 一样, 这里可以直接传sandbox
benchmark(‘vm.runInNewContext’, () => {
vm.runInNewContext(code, sandbox);
});
const script = vm.createScript(code);
benchmark(‘script.runInThisContext’, () => {
script.runInThisContext();
});
benchmark(‘script.runInNewContext’, () => {
script.runInNewContext(sandbox);
});
benchmark(‘script.runInContext’, () => {
script.runInContext(ctx);
});
benchmark(‘fn’, () => {
new Function(‘num’, code)();
});
/*
vm.runInThisContext: 15ms
vm.runInContext: 71ms
vm.runInNewContext: 70ms
script.runInThisContext: 7ms
script.runInNewContext: 59ms
script.runInContext: 57ms
fn: 9ms
script方式都比vm方式快
*/
## 线上应用性能数据首页内容有5-6屏内容,一次性渲染,部分组件动态加载。下图是 Vue 服务端渲染 Render时间:<br />![](/medias/easyjs/blog/blog-xriz8a-4815332.png)从首页render-avg的统计来看,== 模板的编译时间非常的短,平均在24ms-27ms之间==,还有优化的空间。<br />首页访问链路时间<br />![](/medias/easyjs/blog/blog-xriz8a-6520397.png)从整个链路时间来看rt(首屏时间) 可以看到, 平均首屏时间小于1s, 而render时间平均在30ms,在整个链路上面,**render的时间可以说是可以忽略的**,至少从上面图来看,**性能问题大头部分在于网络,接口耗时两部分**。## CPU和内存占用前提条件:- Mac 环境- Nunjucks 和 Vue 渲染都开启缓存- Vue 服务端渲染关闭 _runInNewContext_- 为保证测试的统计准确性,只启动一个工作worker,下面分析只统计 worker进程CPU和内存,排除了 Egg master 和 agent 进程。**Nunjucks CPU和内存占用**- 采集样本:`ab -c 100 -n 50000 http://ip:port/perf/nunjucks/`
This is ApacheBench, Version 2.3 <$Revision: 1663405 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 100.84.250.56 (be patient)
Completed 5000 requests
Completed 10000 requests
Completed 15000 requests
Completed 20000 requests
Completed 25000 requests
Completed 30000 requests
Completed 35000 requests
Completed 40000 requests
Completed 45000 requests
Completed 50000 requests
Finished 50000 requests
Server Software:
Server Hostname: 100.84.250.56
Server Port: 7001
Document Path: /perf/nunjucks/
Document Length: 13899 bytes
Concurrency Level: 100
Time taken for tests: 173.686 seconds
Complete requests: 50000
Failed requests: 48138
(Connect: 0, Receive: 0, Length: 48138, Exceptions: 0)
Total transferred: 709284995 bytes
HTML transferred: 694684887 bytes
Requests per second: 287.88 [#/sec] (mean)
Time per request: 347.372 [ms] (mean)
Time per request: 3.474 [ms] (mean, across all concurrent requests)
Transfer rate: 3988.00 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 10 38.1 2 3410
Processing: 22 336 313.5 241 3416
Waiting: 22 333 302.2 241 3398
Total: 56 347 311.6 247 3432
Percentage of the requests served within a certain time (ms)
50% 247
66% 280
75% 332
80% 367
90% 645
95% 877
98% 1195
99% 1460
100% 3432 (longest request)
![](/medias/easyjs/blog/blog-xriz8a-8067980.png)<br />**Vue CPU和内存占用**<br />**- 采集样本:`ab -c 100 -n 50000 http://ip:port/perf/vue/`
This is ApacheBench, Version 2.3 <$Revision: 1663405 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 100.84.250.56 (be patient)
Completed 5000 requests
Completed 10000 requests
Completed 15000 requests
Completed 20000 requests
Completed 25000 requests
Completed 30000 requests
Completed 35000 requests
Completed 40000 requests
Completed 45000 requests
Completed 50000 requests
Finished 50000 requests
Server Software:
Server Hostname: 100.84.250.56
Server Port: 7001
Document Path: /perf/vue/
Document Length: 13840 bytes
Concurrency Level: 100
Time taken for tests: 193.524 seconds
Complete requests: 50000
Failed requests: 48989
(Connect: 0, Receive: 0, Length: 48989, Exceptions: 0)
Total transferred: 707135621 bytes
HTML transferred: 692535158 bytes
Requests per second: 258.37 [#/sec] (mean)
Time per request: 387.048 [ms] (mean)
Time per request: 3.870 [ms] (mean, across all concurrent requests)
Transfer rate: 3568.35 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 10 27.3 2 1384
Processing: 22 377 223.9 285 2236
Waiting: 22 373 217.7 285 2235
Total: 42 386 219.5 290 2239
Percentage of the requests served within a certain time (ms)
50% 290
66% 335
75% 409
80% 481
90% 697
95% 841
98% 1030
99% 1126
100% 2239 (longest request)dou
![](/medias/easyjs/blog/blog-xriz8a-2015451.png)两个图对比发现如下信息:- 压测前 egg 应用启动后,worker 进程内存稳定在 60MB, cpu 使用都小于1%,保证前提条件一致- 压测启动后,nunjucks 和 vue CPU使用迅速飙升到90%,曲线基本保持一样,没有很明显高低之分- 压测启动后,nunjucks 和 vue 内存也是迅速上升,整个压测期间,nunjucks 平均内存使用为 150 MB左右,vue 平均内存使用为 160 MB, vue 内存占用比较稳定。从这个数据可以看出, Vue 服务端渲染 内存占用略微比 nunjucks 高一些。- 压测结束好后,nunjucks 和 vue 的 CPU 使用 迅速降为小于 1%, 内存使用迅速降为 60 MB, 都恢复为压测前的状态,这也表面 nunjucks 和 vue 压测期间没有出现内存泄漏的情况。## Nunjucks与Vue对比分析**首先我们来看看 ab 执行结果的几个关键参数含义:**- Concurrency Level 并发请求数- Time taken for tests 整个测试持续的时间- Complete requests 完成的请求数- Failed requests 失败的请求数(指内容**大小**不一样,其实是成功的)- HTML transferred HTML内容传输量- Requests per second 每秒处理的请求数,**mean表示平均值**- Time per request 用户平均请求等待时间- Time per request(mean, across all concurrent requests) 服务器平均请求处理时间- Percentage Time 处理时间区间分布,我们关注80%-90%的区间。下面是 ab -c 100 -n 50000 针对 nunjucks 和 vue 数据对比:<br />![](/medias/easyjs/blog/blog-xriz8a-7039687.png)<br />从上图 ab 对比数据可以得出以下结论:- HTML transferred 内容传输量数据非常接近,保证了对比测试的客观性。- 50000个请求,vue 整体处理时间比 nunjucks 慢 20s, 平均每个相当于慢 0.4 ms,这个于 上面 render 数据对比是吻合的。- nunjucks 每秒处理的请求数比 vue 略微多 30 个, 用户平均请求等待时间少 0.4 ms- 从 percentage time 时间我们发现 nunjucks 和 vue 每个区间都是非常接近。总体上,nunjucks 和 vue 在 模板渲染,CPU使用,内存占用没有很明显的差异,各指标基本接近。 其中 nunjucks 在模板渲染方面略微快一点点(个位数毫秒级), 内存占用方面 vue 比 nunjucks 占用略微多一点,但都在可接受范围内。## CPU和内存工具在进行 CPU 和 内存 监控统计分析时,也没有找到简单好用的火焰图工具。Alinode 平台统计粒度太大,数据也不是时时可以拿到,也不好使。找到一些成熟的工具比如 perf 和 FlameGraph 都比较复杂,而且一些资料都是 linux 上面的, 配置相当繁琐,只好放弃。另外找到 Mac 的一个小工具 iStat Menus 能显示电脑磁盘CPU,内存等占用情况不错,图也很小且不适合做具体分析,作为电脑监控工具还不错。最终也没有找到合适简单工具,只好简单撸一个,顺便玩了一把 [socket.io](https://link.zhihu.com/?target=http%3A//socket.io)和图表工具。上面 CPU 和 内存 统计是通过 Egg [egg-socket.io](https://link.zhihu.com/?target=https%3A//github.com/eggjs/egg-socket.io) 和 [egg-schedule](https://link.zhihu.com/?target=https%3A//github.com/eggjs/egg-schedule) 插件, [current-processes](https://link.zhihu.com/?target=https%3A//github.com/branneman/current-processes), [socket.io.js](https://link.zhihu.com/?target=https%3A//github.com/socketio/socket.io-client) 以及图片库 Ignite UI 实现的。- Egg Node端 [egg-socket.io](https://link.zhihu.com/?target=https%3A//github.com/eggjs/egg-socket.io)和[egg-schedule](https://link.zhihu.com/?target=https%3A//github.com/eggjs/egg-schedule)[current-processes ](https://link.zhihu.com/?target=https%3A//github.com/branneman/current-processes)结合```javascript'use strict';const ps = require('current-processes');const os = require('os');const totalMem = os.totalmem(); // bytes to MBmodule.exports = app => { exports.schedule = { interval: '3s', type: 'worker' }; exports.task = function* (ctx) { ps.get((err, processes) => { const proArr = processes.filter(pro => { return pro.name === 'node'; }).sort((a, b) => { return a.pid - b.pid; }); proArr.shift(); proArr.shift(); const cpu_mem_info = proArr.map(pro => { return { pid: pro.pid, cpuPercent: pro.cpu, totalMemory: totalMem, usedMemory: pro.mem.private, // RSS实际内存占用大小 memoryPercent: pro.mem.usage * 100, // 进程占用内存百分比 virtualMemory: pro.mem.virtual, // 虚拟内存占用大小 }; }); ctx.app.io.emit('monitor-memory-cpu', cpu_mem_info); }); }; return exports;};
var socket = io.connect("http://localhost:7001");socket.on("monitor-memory-cpu", function (data) { data.forEach(info => { info.displayCPU = info.cpuPercent; info.displayMem = info.usedMemory; info.displayTime = new Date().toLocaleTimeString(); cpuData.push(info); $("#cpuChart").igDataChart("notifyInsertItem", cpuData, cpuData.length - 1, info); $("#memoryChart").igDataChart("notifyInsertItem", cpuData, cpuData.length - 1, info); });});
easywebpack 3.5.0 新增自定义插件 webpack-manifest-resource-plugin 替换 webpack-manifest-plugin。 之前的 manifest 依赖关系是在 Egg 运行期间解析的,现在改为构建期组装好资源依赖关系。新生成的 manifest 可以在纯前端项目使用,比如 PWA 场景。
// ${app_root}/config/manifest.json{ "app/app.js": "/public/js/app/app.2cf6dfd1.js", "app/app.css": "/public/css/app/app.cda9bc64.css", "common.js": "/public/js/common.b59f7169.js", "common.css": "/public/css/common.cda9bc64.css"}
// ${app_root}/config/manifest.json{ "app/app.js": "/public/js/app/app.2cf6dfd1.js", "app/app.css": "/public/css/app/app.cda9bc64.css", "common.js": "/public/js/common.b59f7169.js", "common.css": "/public/css/common.cda9bc64.css", "deps": { "app/app.js": { "js": [ "/public/js/vendor.337ab787.js", "/public/js/common.b59f7169.js", "/public/js/app/app.2cf6dfd1.js" ], "css": [ "/public/css/common.cda9bc64.css", "/public/css/app/app.cda9bc64.css" ] }}
项目需要安装 npm install react-hot-loader –save-dev 依赖
{ "env": { "development": { "plugins": [ "react-hot-loader/babel" ] } }}
app.js
'use strict';import React, { Component } from 'react'import ReactDOM from 'react-dom'import { Provider } from 'react-redux'import { AppContainer } from 'react-hot-loader';import createStore from './store';import Routes from './router'import './app.css';const App = () => { return EASY_ENV_IS_DEV ? <AppContainer><Routes /></AppContainer> : <Routes />;};const Entry = () => (<div> <Provider store={ createStore() }> <App /> </Provider></div>);ReactDOM.render(<Entry />, document.getElementById('app'));if (EASY_ENV_IS_DEV && module.hot) { module.hot.accept();}
https://github.com/easy-team/easywebpack-awesome/tree/master/boilerplate/react
https://github.com/keenwon/Egg-Webpack-Starter
vue 项目无需额外配置,直接使用 Webpack 热更新配置 和 启动 koa webpack 编译服务即可
https://github.com/easy-team/easywebpack-awesome/tree/master/boilerplate/vue
在具体项目开发时,不需要自己实现热更新配置和启动服务,这些都已经集成到 easywebpack 体系里面。
**
转化为 webpack 配置
module.exports = { entry: { app: [ "webpack-hot-middleware/client?path=http://127.0.0.1:9000/__webpack_hmr&noInfo=false&reload=true", "src/index.js" ] }, plugins: [new webpack.HotModuleReplacementPlugin()]}
const webpack = require('webpack');const webpackConfig = require('./webpack.config');const compiler = webpack(webpackConfig);app.use(require("webpack-dev-middleware")(compiler, { noInfo: true, publicPath: webpackConfig.output.publicPath}));app.use(require("koa-webpack-hot-middleware")(compiler));
使用easy-cli你将得到一个具备以下能力的骨架项目:
CMD
的脚本依赖注入方式。npm i easywebpack-cli -g
easy init
按照指引选择/输入对应内容
进入骨架项目目录
npm start
Enjoy It Easily~
构建生产环境内容。
npm build
你可以在easy-cli生成的骨架项目中看到这样的配置内容。
基础配置含义可以参考,通用的基础配置介绍。
(注:有时我们需要获得webpack的原生能力。我们可以借助额外配置的方式直接和webpack沟通。)
const path = require('path');module.exports = { framework: 'html', // 指定用 easywebpack-html 解决方案, 请在项目中安装该依赖 entry: 'src/**/*.js', externals: { jquery: 'window.$' }, module: { rules: [ { scss: true }, { nunjucks: { options: { searchPaths: ['./widget'] // 配置查找模板路径 } } ] }};
src/view/layout.html
template: 'src/view/layout.html'
easy dev
easy build
// webpack.config.jsconst easywebpack = require('easywebpack-html');const webpack = easywebpack.webpack;const merge = easywebpack.merge;const env = process.env.BUILD_ENV;const baseWebpackConfig = easywebpack.getWebpackConfig({ env, // 根据环境变量生成对应配置,可以在 npm script 里面配置,支持dev, test, prod 模式 entry: { home: 'src/page/home/home.js' }, module: { rules: [ { scss: true }, { nunjucks: { options: { searchPaths: ['./widget'] // 配置查找模板路径 } } ] }});// 拿到基础配置, 可以进行二次加工const webpackConfig = merge(baseWebpackConfig, { // 自定义配置})module.exports = webpackConfig;
webpack-dev-server --hot
easy dev --webpack
webpack --mode production --config webpack.config.js
easy build --webpack
v3
代表 easywebpack 3.x.x, v4
代表 easywebpack 4.x.xeasywebpack 内置了 babel
, eslint
, css
, sass
, less
, stylus
, urlimage
, urlfont
等loader,
easywebpack-vue 内置了 vue
, vuehtml
等loader,
easywebpack-react 内置了 react-hot-loader
等loader,
easywebpack-weex 内置了 vue
, weex
等loader.
easywebpack-html 内置了 html
, nunjucks
等loader.
loader | 别名 | 默认是否开启 | webpack.config.js配置举例 |
---|---|---|---|
babel-loader | babel | 是 | 禁用: loaders:{ babel: false} |
eslint-loader | eslint | 否 | 启用: loaders: { eslint: true} 自动修复: loaders:{ eslint: {options: {fix: true}} |
tslint-loader | tslint | 否 | 启用: loaders:{ tslint: tue} 自动修复: loaders:{ tslint: {options: {fix: true} |
ts-loader | ts | 否 | 禁用: loaders:{ ts: false} 开启: loaders:{ ts: true}** |
css-loader | css | 是 | N/A |
sass-loader | sass | v3 是 v4 否 | 开启: ** loaders:{ sass: true} **路径配置: loaders:{sass: {options: {includePaths: [“asset/css”]}} 安装依赖: “node-sass“: “^4.5.3”, “sass-loader“: “^6.0.6”, |
sass-loader | scss | v3 是 v4 否 | 开启: loaders:{ scss: true} 安装依赖: “node-sass“: “^4.5.3”, “sass-loader“: “^6.0.6”, |
less-loader | less | 否 | 开启: loaders:{ less: true} 安装依赖: “less“: “^2.7.2”, “less-loader“: “^4.0.5”, |
stylus-loader | stylus | 否 | 开启: loaders:{ stylus: true } “stylus”: “^0.54.5”, “stylus-loader”: “^3.0.0”, |
url-loader | urlimage | 是 | 禁用: loaders:{ urlimage: false} 配置limit(默认1024): loaders:{urlimage: {options: {limit: 2048 }} |
url-loader | urlfont | 是 | 禁用: loaders:{ urlfont: false} 配置limit(默认1024): loaders:{urlfont: {options: {limit: 2048 }} |
url-loader | urlmedia | 是 | 禁用: loaders:{ urlmedia: false} 配置limit(默认1024): loaders:{urlmedia: {options: {limit: 2048 }} |
nunjucks-html-loader | nunjucks | 否 | 启用: loaders:{ nunjucks: true } |
ejs-loader | ejs | 否 | 启用: loaders:{ ejs: true } |
// ${app_root}/webpack.config.jsmodule.exports = { module:{ rules:[ { ${loader别名}:{ options:{ // 具体loader参数 } } } ] }}或module.exports = { loaders:{ ${loader别名}:{ options:{ // 具体loader参数 } } }}
module: { rules: [ { test: /\.tsx?$/, loader: "ts-loader" } ]}
// 最新版本建议配置module: { rules: [ { ts: true } ]}
config.loaders | config.module.rules 非必须,支持 Object | Array。 这里的loaders 是对 Webpack module.rules
的简化和增强。建议用 增强配置 方式配置.
兼容 Webpack 原生数组配置
[增强]支持通过别名对内置 loader 开启和禁用,以及参数配置
[增强]支持通过别名的方式添加 loader 插件
// ${app_root}/webpack.config.js// 最新版本建议配置module: { rules: [ { ${loader别名}:{ include:[], options:{ // 具体loader参数 } } } ]}
// ${app_root}/webpack.config.jsmodule.exports = { ...... module: { rules:[ { ts: true }, { test: /\.html$/, use: ['html-loader', 'html-swig-loader'] } ]}
// ${app_root}/webpack.config.js// 最新版本建议配置module.exports = { ...... module: { rules: [ { ts: true }, { less: true } ] }}
// ${app_root}/webpack.config.jsmodule.exports = { module:{ rules:[ { eslint:{ options:{ fix: true } } } ] }}
// ${app_root}/webpack.config.jsconst path = require('path');module.exports = { module:{ rules:[ { sass: { options: { includePaths: [ path.resolve(process.cwd(), 'app/web/asset/style') ] } } }, { scss: { options: { includePaths: [ path.resolve(process.cwd(), 'app/web/asset/style') ] } } } ] }}
// ${app_root}/webpack.config.jsmodule.exports = { module:{ rules:[ { vue: { options: { transformToRequire: { img: ['url', 'src'] } } } } ] }}
module.rules : {Object} Webpack loader 配置, 支持自定义格式和原生格式key:value
形式, 其中 key
为别名, 可以自由定义, easywebpack和对应解决方案内置了一些别名和loader.
比如我要添加一个全新且 easywebpack 没有内置的 html-swig-loader, 可以这样配置:
// 最新版本建议配置module.exports = { ...... module: { rules: [ // 内置 loader 和 原生 loader 混合配置 { ts: true }, { test: /\.html$/, use: ['html-loader', 'html-swig-loader'] } ] }}
swig
key 别名随意, 我可以叫 swig, 也可以叫 htmlswig 等等
babel-loader
可以这样配置// 最新版本建议配置module.exports = { ...... module: { rules: [ { babel:false } ] }}
babel-loader
的 test 和 use, 可以这样配置
因 use 存在顺序问题, use 目前采用的策略是完全覆盖
// 最新版本建议配置module.exports = { ...... module: { rules: [ { babel: false }, // 禁用默认 { // 自己配置 test: /\.(jsx|vue)?$/, exclude: [/node_modules/, 'page/test'], use: [ { loader: 'babel-loader' }, { loader: 'eslint-loader' } ] } ] }}
config.loader 配置项除了支持的loader原生属性, 还扩展了 env
, type
, enable
, postcss
, framework
五个属性, 其中 postcss
, framework
用于css相关loader, 例如内置的 sass-loader
// 最新版本建议配置module.exports = { ...... module: { rules: [ { sass: false }, // 禁用默认 { // 自己配置 test: /\.sass/, exclude: /node_modules/, use: ['css-loader', { loader: 'sass-loader', options: { indentedSyntax: true } }] } ] }}
env: 见 config.env
说明, 可选, 默认全部
type: 见 config.type
说明, 可选, 默认全部
enable: {Boolean/Function} 是否启用, 可选, 默认可用
postcss: {Boolean} 可选, 特殊配置, 是否启用postcss, 只有css样式loader需要配置, 其他loader不需要配置
use: {Array/Function} 必须, 支持扩展的Function配置和原生Use配置, use属性是完全覆盖.
为了更方便升级 Babel7, 同时尽量减少配置且无需安装 @babel 依赖,@easy-team 模式直接内置Babel 7 的相关依赖,只需要把 easywebpack 依赖模式改成 @easy-team/easywebpack 模式,如果代码中直接依赖了也请一并修改。
@easy-team/easywebpack-cli: ^4.0.0 替换 easywebpack-cli
@easy-team/easywebpack-react: ^4.0.0 替换 easywebpack-react
@easy-team/easywebpack-vue: ^4.0.0 替换 easywebpack-vue
Webpack 4 压缩是通过 optimization.minimizer 来实现,默认 console 是没有被移除,如果需要移除,可以通过TerserPlugin 配置解决。
const TerserPlugin = require('terser-webpack-plugin');'use strict'// webpack.config.jsmodule.exports = { optimization: { minimizer: [ new TerserPlugin({ terserOptions: { compress: { drop_console: true } } }) ] }}
在通过 webpack 构建 server side render 模块(target: ‘node’)时,默认情况下,node_modules 下的模块不会打进构建后的 JSBundle 文件里,而是直接通过 require 方式引用模块的。如果在业务代码中直接引用 node-modules 下的 .vue 或 .jsx 文件时,会出现不能解析引入的组件模块。这是需要通过
nodeExternals.whitelist
配置该模块需要编译到 JSBundle 文件里面,这样才能被相应 loader 进行编译处理。
// ${root}/webpack.config.jsconst path = require('path');const resolve = filepath => path.resolve(__dirname, filepath);module.exports = { nodeExternals: { whitelist: [ moduleName => { return /iview/.test(moduleName); }] }, module: { rules: [ { babel: { include: [resolve('app/web'), resolve('node_modules/iview')], exclude: [] }, }, { vue: { include: [resolve('app/web'), resolve('node_modules/iview')], exclude: [], } } ] },};
customize
钩子处理 //${root}/webpack.config.jsmodule.exports = {...customize(webpackConfig){ // 此外 webpackConfig 为原生生成的 webpack config,可以进行自定义处理 return webpackConfig;}}
// ${root}/config/config.local.jsmodule.exports = app => { const exports = {}; exports.webpack = { browser: false, // 这里可以打开指定 url 地址 }; return exports;};
// ${root}/webpack.config.jsmodule.exports = { devServer: { open: 'http://127.0.0.1:8888' }};
.babelrc 配置 babel 装饰器编译插件: https://babeljs.io/docs/en/babel-plugin-proposal-decorators
方式一:改为 import 方式
增加 css importLoaders 配置
// ${root}/webpack.config.jsmodule.exports = { loaders: { css: { importLoaders: 1 } } }
在用 TypeScript 编写 Vue 应用时, Vue 里面的 TypeScript 代码建议不要写在 Vue 文件里面,请以单独 ts 文件存放 TypeScript 代码。目前测试发现与 thread-loader 一起使用是有问题的。easywebpack 4.10.0 开始,默认开启了 thread-loader, 你可以通过如下方式禁用 thread-loader:
// ${root}/webpack.config.jsmodule.exports = { compile: { thread: false } }
目前 easywebpack 3 默认是 开启了 sass 功能,但安装 node-sass
时, 会出现安装不成功(二进制本地编译)的情况,这个时候可以按照如下方式禁用 node-sass . easywebpack 4 已默认禁用。
确保代码引用里面没有用 sass 编写样式
删除 package.json
里面的 node-sass
依赖
禁用 webpack 引用 node-sass
编译
// ${app_root}/webpack.config.jsmodule.exports= { module: { rules:[ { scss: false } ] }};
目前 easywebpack 默认是打正式包时开启了图片压缩功能,但在某些部分机器安装 imagemin-webpack-plugin
时, 会出现安装不成功的情况(二进制本地编译, 系统缺少某些本地库),这个时候可以按照如下方式禁用 imagemin-webpack-plugin
删除 package.json
里面的 imagemin-webpack-plugin
依赖
禁用 webpack 引用 imagemin-webpack-plugin
编译
// ${app_root}/webpack.config.jsmodule.exports= { plugins :[ { imagemini: false } ]};
easy 体现 默认 node_modules 是被 babel 排除的,如果有 es6+ 的模块,需要包含进来才行,否则压缩报错
// ${app_root}/webpack.config.jsmodule.exports = { module: { rules:[ { babel: { exclude: [] } } ] }}
easywebpack-vue
默认的 vue-loader
配置排除了 node_moudles
目录, 主要目的是避免 node_moudles
被扫描,加快构建速度。如果你需要 引入 node_moudles
下 vue 组件, 请把对应的组件加入 include
配置 或者 用 exclude
覆盖默认配置,建议include
配置.
include
配置
例如: 代码在 app/web 目录, 引入 node_modules 下 vue 组件为
// ${app_root}/webpack.config.jsconst path = require('path');const resolve = filepath => path.resolve(__dirname, filepath);module.exports= { module: { rules:[ { vue: { include: [resolve('app/web'), /node_module\/vue-datepicker-local/] } } ] }}
相关issue: import 外部模块失败
exclude
配置// ${app_root}/webpack.config.jsmodule.exports = { module: { rules:[ { babel: { exclude: [] } } ] }}
see: https://github.com/webpack/webpack/issues/2031
在 Egg + Vue/React 解决方案中, Webpack publicPath 使用的是默认 publicPath: '/public/'
配置。
如果要修复默认的publicPath,比如要修改 /static/
,需要修改三个地方:
easywebpack: ^3.5.1
egg-webpack: ^3.2.5
webpack.config.js
配置添加 publicPath
配置覆盖默认配置// ${app_root}/webpack.config.jsmodule.exports = { ..... output: { path: 'static', publicPath: '/static/ }};
config.default.js
添加静态资源// ${app_root}/config/config.local.js exports.static = { prefix: '/static/', dir: path.join(app.baseDir, 'static') };
// ${app_root}/config/config.local.js exports.webpack = { proxy: { match: /^\/static\// } };
npm install
安装后, npm start
启动失败
在使用 easywebpack
体系构建时, 在首次 npm start
时, easywebpack
会检查开启的 loader, plugin 插件是否已经安装, 如果没有安装则自动安装.
在这个过程会打印动态安装的 npm
模块, 如果安装失败则会导致启动失败, 这个时候你可以手动安装动态安装的 npm
模块 或者通过 easy install
自动动态安装缺失的依赖, 同时把依赖写入 package.json
的 devDependencies
中.
然后重新启动.easywebpack
解决方案只内置了必须的几个常用 loader 和 plugin, 其他 loader (比如 less, stylus) 和 plugin (imagemini) 都是需要项目自己根据需要安装。如果你自己搭建项目,遇到依赖缺失错误,除了手动 npm install 安装以外, 可以使用 easy install
命令,安装所有缺失的依赖,默认是 npm
方式
easy install
通过 mode
参数指定 cnpm
方式安装依赖(前提是你全局安装了cnpm)
easy install --mode cnpm
这里采用动态安装是因为如果把所有插件都内置, 会导致安装很多无用的 npm
模块, 安装缓慢, 更严重的是有些 loader
, plugin
如果出现问题, 则导致整个 easywebpack
体系不能用.
注意: 该问题已在最新版本 easywebpack@4.8.0解决方案中已自动检测端口占用问题,无需配置。
Egg 应用本地开发时, npm run dev 默认启动打开浏览器的端口是 7001, 如果要修改自动打开的端口为6001, 可以在 config/config.local.js
中 添加 端口配置
// ${app_root}/config/config.local.jsexports.webpack = { appPort: 6001 webpackConfigList: EasyWebpack.getWebpackConfig()};
egg-webpack
启动打开浏览器的取端口逻辑: this.config.webpack.appPort || process.env.PORT || 7001
注意: 该问题已在最新版本 easywebpack@4.8.0 解决方案中已自动检测端口占用问题,无需配置。
在 Egg + Webpack 项目开发过程中, 会用到 7001, 9000, 9001 三个端口
7001 是 Egg 应用启动的默认端口
9000, 9001 是 Webpack 启动 Webpack dev server 的端口, 9000 为 构建前端渲染js, 9001 构建后端渲染逻辑.
如果有两个项目同时开发, 第二个项目需要修改这三个端口, 假如 egg 应用: 5000, Webpack 构建 9100 和 9101
Egg 应用默认会读取 process.env.PORT
变量, 这里我们新起一个环境变量或者直接写 5000
// ${app_root}/index.jsrequire('egg').startCluster({ baseDir: __dirname, port: process.env.EGG_PORT || 5000});
// ${app_root}/config/config.local.jsexports.webpack = { port: 9100, proxy: { host: 'http://127.0.0.1:9100' } webpackConfigList: EasyWebpack.getWebpackConfig()};
webpack.config.js
的 port 配置// ${app_root}/webpack.config.jsmodule.exports = { port: 9100, ......};
async/await
特性时, 报错:regeneratorRuntime is not defined。
目前骨架前端是没有用 async/await
,所以没有内置。有需要的自己可以在 .bablerc 文件加 transform-runtime
,同时安装对应依赖到 devDependencies
中即可。
npm install babel-plugin-transform-runtime --save-dev
// ${app_root}/.bablerc{ 'plugins':['transform-runtime']}
因本地开发时,图片没有hash,如果存在相同的图片名称, 就会存在覆盖问题。目前可以通过开启本地开发图片 hash 临时解决。
// ${app_root}/webpack.config.jsmodule.exports= { imageHash: true};
开发期间禁用 Chrome Network控制面板网络缓存, Disable cache 勾选上
运行 npx easy clean all 或 easy clean all (cli)
Error: ENOENT: no such file or directory, scandir '{PATH}\node-sass\vendor' at Error (native) at Object.fs.readdirSync (fs.js:856:18) at Object.getInstalledBinaries ({PATH}\node_modules\.npminstall\node-sass\3.7.0\node-sass\lib\extensions.js:74:13) at foundBinariesList ({PATH}\node_modules\.npminstall\node-sass\3.7.0\node-sass\lib\errors.js:20:15) at foundBinaries ({PATH}\node_modules\.npminstall\node-sass\3.7.0\node-sass\lib\errors.js:15:5) at Object.module.exports.missingBinary ({PATH}\node_modules\.npminstall\node-sass\3.7.0\node-sass\lib\errors.js:45:5) at Object.<anonymous> ({PATH}\node_modules\.npminstall\node-sass\3.7.0\node-sass\lib\index.js:14:28) at Module._compile (module.js:413:34)
这时需要重新编译 node-sass: npm rebuild node-sass
解决
前提:
代理域名能够映射到本机ip地址的功能需要你自己在电脑上面配置。如果是实际的存在的域名,理论上面就不需要自己配置域名映射。
该功能只在 Egg 应用构建本地开发使用。
在 Egg SSR 应用开发时,Egg 应用的访问地址, 静态资源构建的地址, HMR 地址都是 ip, 不方便进行环境模拟测试,比如 cookie和登陆场景。
// webpack.config.jsmodule.exports = { host: 'http://app.debug.com' // 只在 env: dev 方式生效, 这里 host 改成你自己的实际有效的域名地址。}
应用访问的地址是: http://app.debug.com
在日常本地开发时,我们经常会遇到以下情况:
在H5本地开发页面, 经常遇到白名单(APP里面, 外部平台)和cookie问题
同样的包线上环境有问题,本地OK, 需要模拟线上环境
在这样的情况下,我们可以通过nginx和dnsmasq搭建本地搭建代理服务器, 把线上的域名请求映射到本机解决以上两个问题.
如果mac系统,默认时安装了nginx, 可以通过 http://127.0.0.1 检查 nginx是否正常, 如果正常会显示 Welcome to nginx 信息<br />
brew search nginx
brew install nginx
brew uninstall nginx
sudo brew update
sudo brew info nginx
brew list
sudo brew services start nginx
nginx -v
sudo brew services stop nginx
nginx -s reload
nginx -s stop
进入nginx安装目录 /usr/local/etc/nginx
, 我们看到servers目录下面有个 default.conf
配置80端口映射, 访问http://127.0.0.1时,会自动打开root对应目录的index.html文件
nginx启动时,会自动读取 /usr/local/etc/nginx/server
目录所有的server配置文件,文件名可以自由定义(在 nginx.conf 里面include servers/*)
server { listen 80 default_server; index index.html; root /usr/local/var/www; server_name 127.0.0.1;}
自定义配置, 比如我想把 local.sky.com转发到本机的 http://127.0.0.1:5000
地址, 只需要在server目录下面增加文件5000.conf(文件名可自定义),然后增加一下配置:
server { listen 80; server_name local.sky.com local.sky.cn; location / { proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:5000; } access_log /Users/caoli/dev/log/proxy.log;}
127.0.0.1 localhost::1 localhost127.0.0.1 local.sky.com127.0.0.1 local.sky.cn
最后通过 nginx -s reload
命令重启nginx, 然后在浏览器地址栏就可以通过 local.sky.com 访问 http://127.0.0.1:5000了. 通过以上配置就可以解决电脑端域名映射和cookie等问题. 如果不配置 nginx,可以通过 http://proxy.test.cn:5000 访问, 配置 nginx 后,可以直接 local.sky.com 访问。
server { listen 80; server_name local.sky.com local.sky.cn; location / { proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:7001; } access_log /usr/local/etc/nginx/logs/local-7001.log;} # HTTPS serverserver { listen 443 ssl; server_name local.sky.com local.sky.cn; ssl_certificate /usr/local/etc/nginx/server.crt; ssl_certificate_key /usr/local/etc/nginx/server.key; ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; location / { proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:7001; }}
brew install dnsmasq
sudo brew services restart dnsmasq
拷贝并重命名/usr/local/opt/dnsmasq/dnsmasq.conf.example -> /usr/local/etc/dnsmasq.conf。
cp /usr/local/opt/dnsmasq/dnsmasq.conf.example /usr/local/etc/dnsmasq.conf
cd /usr/local/etcvim resolv.dnsmasq.conf
nameserver 8.8.8.8nameserver 8.8.4.4
修改 /usr/local/etc/dnsmasq.conf
的 resolv-file
, address
, listen-address
, strict-order
, no-hosts
配置项, 如果没有请添加, 如果是#注释,请取消注释
#no-hostsno-hosts#strict-orderstrict-order#resolv-fileresolv-file=/usr/local/etc/resolv.dnsmasq.conf# web-serveraddress=/local.sky.com/192.168.1.1address=/local.sky.cn/192.168.1.1# listen-address 192.168.1.1 为本机iplisten-address=127.0.0.1,192.168.1.1
sudo cp -fv /usr/local/opt/dnsmasq/*.plist /Library/LaunchDaemonssudo lauchctl load /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plistsudo launchctl stop homebrew.mxcl.dnsmasqsudo launchctl start homebrew.mxcl.dnsmasqsudo killall -HUP mDNSResponder
sudo brew services start dnsmasq
curl 127.0.0.1 -H "Host:proxy.test.com"
dig proxy.test.com @0.0.0.0
Android 手机使用DNS服务, 请安装Fast DNS Change APK, 把自己的本机IP添加到DNS列表中,如果需要用本机DNS,请双击会显示已Connnected到本机DNS, 再次点击Disconnnected
https://apkpure.com/cn/fast-dns-changer-no-root/com.mustafademir.fastdnschanger
iOS 手机使用DNS服务, 把自己本机的ip填写到 DNS列表中, DNS的地址之间要用逗号间隔一下.
http://jingyan.baidu.com/article/dca1fa6f44c664f1a5405244.html
如果你愿意付费,可以安装个IOS APP: DNS Override,可以一键开启 dns 设置.
PC访问
http://jingyan.baidu.com/article/fc07f9891f626712ffe519cf.html
DNS配置以后, 就可以在手机上面通过域名(http://proxy.test.com:5000 和 http://proxy.test1.com:5000) 访问, 然后映射到本机服务 http://127.0.0.1:5000.
easywebpack 4 版本支持了本地接口代理转发的功能, 主要解决本地开发跨域问题. 具体见:/easywebpack/ed847g
]]>npm install easywebpack-cli -g
${app_root}/.babelrc
文件具体根据实际情况添加相关 babel 插件配置,以下仅仅是举例. 详细配置见:/easywebpack/babel
{ "presets": [ "@babel/preset-react", [ "@babel/preset-env", { "modules": false } ] ], "plugins": [ "@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-object-rest-spread", ]}
{ "presets": ['react',["env",{ "modules": false }]], "plugins": [ "transform-object-rest-spread", "syntax-dynamic-import" ], "comments": false}
${app_root}/postcss.config.js
文件具体根据实际情况添加 postcss 配置,以下仅仅是举例:
'use strict';module.exports = { plugins: [ require('autoprefixer') ]};
默认 template 路径文件为 src/view/layout.html
如果需要构建 HTML 文件,直接存在该文件即可,无需 Webpack 配置。
<!DOCTYPE html><html lang="en"><head> <title></title> <meta http-equiv="content-type" content="text/html;charset=utf-8"> <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" /></head><body> <div id="app"></div></body></html>
easywebpack-react 项目构建解决方案,支持前端和SSR模式构建。默认 HtmlWebpackPlugin 的 template 路径为 src/view/layout.html
。
快速获取 react Webpack 构建配置
const easywebpack = require('@easy-team/easywebpack-react');const webpackConfig = easywebpack.getWebpackConfig({ target: 'web', entry:{ app: 'src/index.js' }});
快速获取 React SSR 模式 Webpack 构建配置
const easywebpack = require('@easy-team/easywebpack-react');// 返回的是两个 webpack 配置const webpackConfigList = easywebpack.getWebpackConfig({ entry:{ index: 'src/index.js' }});
// webpack.config.jsconst easywebpack = require('@easy-team/easywebpack-react');const { webpack, merge } = easywebpack.webpack;const env = process.env.BUILD_ENV;const baseWebpackConfig = easywebpack.getWebpackConfig({ env, // 根据环境变量生成对应配置,可以在 npm script 里面配置,支持 dev, test, prod 模式 target : 'web', // target: web 表示只获取前端构建 Webpack 配置 entry:{ index: 'src/index.js' }});// 拿到基础配置, 可以进行二次加工const webpackConfig = merge(baseWebpackConfig, { // 自定义配置})module.exports = webpackConfig;
webpack-dev-server --hot
easy dev --webpack
webpack --mode production --config webpack.config.js
easy build --webpack
easywebpack-cli
插件npm i easywebpack-cli -g
安装成功以后, 就可以在命令行中使用 easy
或 easywebpack
命令, 比如 easy build
, easy server
, easy print
等
webpack.config.js
配置在项目根目录添加 webpack.config.js
文件, 添加如下配置
const path = require('path');module.exports = { target:'web', framework: 'react', // 指定用 easywebpack-react 解决方案, 请在项目中安装该依赖 entry: { index: 'src/index.js' }};
easy build deveasy build testeasy build prod
easy dev# 构建文件并启动本地静态 HTTP Servereasy build --server
运行完成自动打开编译结果页面 : http://127.0.0.1:8888/debug
easywebpack-cli
插件npm i easywebpack-cli -g
easy init
命令初始化骨架项目, 根据提示选择对应的项目类型即可.// ${root}/package.json{ "devDependencies": { "babel-plugin-import": "^1.0.0" }}
import { Button } from 'antd';
{ "presets": [ "@babel/preset-react", [ "@babel/preset-env", { "modules": false } ] ], "plugins": [ "@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-object-rest-spread", ["import", { "libraryName": "antd", "libraryDirectory": "lib", "style": true }] ], "env": { "development": { "plugins": [ "react-hot-loader/babel" ] } },}
// ${root}/webpack.config.jsmodule.exports = { module:{ rules:[ { less: true } ] }}
// ${root}/package.json{ "devDependencies": { "less": "^2.7.2", "less-loader": "^4.1.0" }}
//${root}/webpack.config.jsconst path = require('path');const resolve = (filepath) => path.resolve(__dirname, filepath);module.exports = { loaders: { babel: { include: [resolve('app/web'), resolve('node_modules')] }, less: { include: [resolve('app/web'), resolve('node_modules')], options: { javascriptEnabled: true, modifyVars: { 'primary-color': 'red', 'link-color': '#1DA57A', 'border-radius-base': '2px' } } } }};
easywebpack: ^4.x.x 或 @easy-team/easywebpack: ^4.0.0
easywebpack-cli: ^4.x.x 或 @easy-team/easywebpack-cli: ^4.0.0
easywebpack-vue: ^4.x.x 或 @easy-team/easywebpack-cli: ^4.0.0
easywebpack-react: ^4.x.x 或 @easy-team/easywebpack-cli: ^4.0.0
easywebpack-html: ^4.x.x
easywebpack-js: ^4.x.x
egg-webpack: ^4.x.x
webpack-tool: ^4.x.x
https://github.com/hubcarl/easywebpack/blob/master/CHANGELOG.md
easy server -d mock
mock 为目录, 默认为 http://localhost:8888。 可以配合 devServer 的 proxy 使用的 pathRewrite 功能为项目提供数据服务。
http://localhost:8888/api/v1/news/list或http://localhost:8888/api/v1/news/listhttp://localhost:8888/api/v1/user/info或http://localhost:8888/api/v1/user/info.json
module.exports = { entry: { index: './src/app.js' }, devServer: { proxy: { '/api/test': { target: 'http://localhost:8888', pathRewrite: {'/api/test' : '/api'} } } }};
为了更方便升级 Babel7, 同时尽量减少配置且无需安装 @babel 依赖,@easy-team 模式直接内置Babel 7 的相关依赖,只需要把 easywebpack 依赖模式改成 @easy-team/easywebpack 模式,如果代码中直接依赖了也请一并修改。更多详细信息见:https://www.yuque.com/easy-team/easywebpack/babel7 。
module.exports = { "env": { "node": { "presets": [ [ "@babel/preset-env", { "modules": false, "targets": { "node": "current" } } ] ], "plugins": [ "@babel/plugin-syntax-dynamic-import" ] }, "web": { "presets": [ [ "@babel/preset-env", { "modules": false, "targets": { "browsers": [ "last 2 versions", "safari >= 8" ] } } ] ], "plugins": [ "@babel/plugin-proposal-object-rest-spread", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-transform-object-assign" ] } }}
easy server
命令启动 HTTP Static Server 展示文件目录导航easywebpack 4.11.0 修复 vue-loader 和 ts-loader 新版本问题
easywebpack 4.11.0 loaders.typescript 改成 loaders.ts
easywebpack 4.10.0 新增 filter: webpack-filter-warnings-plugin
插件
easywebpack 4.10.0 支持公共 css 提取,具体见公共提取
easywebpack 4.10.0 支持 config.devServer 配置,支持 historyApiFallback 和 proxy 配置
Vue TypeScript Node Isomorphic Framework ves, ves-cli, ves-admin
babel 提供默认配置, 详见 https://www.yuque.com/easy-team/easywebpack/babel
easywepback ^4.9.0 支持 SSR babel 按需配置,提高构建速度,详见 Egg + Vue 和 Egg + React
easywepback ^4.9.0 提供 customize 方法 对生成的 webpack 直接进行加工处理
//${root}/webpack.config.jsmodule.exports = { ... customize(webpackConfig){ // 此外 webpackConfig 为原生生成的 webpack config return webpackConfig; }}
thread-loader
和 cache-loader
, 加快构建速度;如果出现问题,请通过如下方式开启和禁用//${root}/webpack.config.jsmodule.exports = { compile: { thread:false, cache:false }}
eslint-loader
, 加快构建速度, 可以通过如下方式开启和禁用//${root}/webpack.config.jsmodule.exports = { loaders;{ eslint: true }}
https://github.com/easy-team/egg-react-webpack-boilerplate/issues/11
https://github.com/easy-team/egg-react-webpack-boilerplate/tree/antd-theme
*配置增加 支持 plugins:[] 和 {}
混合模式配置 *(easywebpack@4.8.5)
*配置增强 集成 copy-webpack-plugin
插件, 通过 plugins.copy
配置 *(easywebpack@4.8.5)
配置简化 egg 项目 egg
无需配置,解决方案支持自动检测功能(easywebpack@4.8.0)
配置简化 egg 项目 framework
无需配置,解决方案支持自动检测功能(easywebpack@4.8.0)
配置简化 entry 提供默认值配置, 为 { index: src/app.js }
(easywebpack@4.8.0)
配置增强 entry 支持 node-glob 模式配置 (easywebpack@4.8.0)
配置简化 entry 支持 template loader 配置 (easywebpack@4.8.0)
配置简化 webpack.config.js
devtool 配置只在本地 dev 模式有效, 默认为 eval。可以通过 easy build --devtool
强制指定 devtool (easywebpack-cli@4.0.0)
配置简化 postcss.conf.js 提供默认配置 (easywebpack@4.8.0)
配置简化 easy build 默认 prod
发布模式(easywebpack-cli@4.0.0)
*配置简化 *babel 相关的 devDependencies 依赖 解决方案已内置,无需项目显示配置依赖 (easywebpack@4.8.0)
开发增强 **自动解决本地开发时端口占用问题,支持多项目自动获取新的端口号 **(easywebpack-cli@4.0.0 和 easywebpack@4.8.0)
easy build
easy server
easy build --server
easy build --webpack
easy build --speed
https://www.yuque.com/easy-team/easywebpack/cssmodule
https://www.yuque.com/easy-team/ves/babel
sass-loader
, easywebpack4 默认禁用 sass-loader
, 如果要开启:loaders:{ sass: true}
webpack.config.js
easywebpack4 plugins 节点配置简化, 无需 args
节点npm install easywebpack-cli -g
${app_root}/.babelrc
文件具体根据实际情况添加相关 babel 插件配置,以下仅仅是举例. 详细配置见:/easywebpack/babel
{ "presets": [ [ "@babel/preset-env", { "modules": false } ] ], "plugins": [ "@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-object-rest-spread", ]}
{ "presets": [["env",{ "modules": false }]], "plugins": [ "transform-object-rest-spread", "syntax-dynamic-import" ], "comments": false}
${app_root}/postcss.config.js
文件具体根据实际情况添加 postcss 配置,以下仅仅是举例:
'use strict';module.exports = { plugins: [ require('autoprefixer') ]};
默认 template 路径文件为 src/view/layout.html
如果需要构建 HTML 文件,直接存在该文件即可,无需 Webpack 配置。
<!DOCTYPE html><html lang="en"><head> <title></title> <meta http-equiv="content-type" content="text/html;charset=utf-8"> <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" /></head><body> <div id="app"></div></body></html>
easywebpack-vue 项目构建解决方案,支持前端和SSR模式构建。默认 HtmlWebpackPlugin 的 template 路径为 src/view/layout.html
。
快速获取 Vue Webpack 构建配置
const easywebpack = require('easywebpack-vue');const webpackConfig = easywebpack.getWebpackConfig({ target: 'web', entry:{ app: 'src/index.js' }});
快速获取 Vue SSR 模式 Webpack 构建配置
const easywebpack = require('easywebpack-vue');// 返回的是两个 webpack 配置const webpackConfigList = easywebpack.getWebpackConfig({ entry:{ index: 'src/index.js' }});
// webpack.config.jsconst easywebpack = require('easywebpack-vue');const { webpack, merge } = easywebpack.webpack;const env = process.env.BUILD_ENV;const baseWebpackConfig = easywebpack.getWebpackConfig({ env, // 根据环境变量生成对应配置,可以在 npm script 里面配置,支持 dev, test, prod 模式 target : 'web', // target: web 表示只获取前端构建 Webpack 配置 entry:{ index: 'src/index.js' }});// 拿到基础配置, 可以进行二次加工const webpackConfig = merge(baseWebpackConfig, { // 自定义配置})module.exports = webpackConfig;
webpack-dev-server --hot
easy dev --webpack
webpack --mode production --config webpack.config.js
easy build --webpack
easywebpack-cli
插件npm i easywebpack-cli -g
安装成功以后, 就可以在命令行中使用 easy
或 easywebpack
命令, 比如 easy build
, easy server
, easy print
等
webpack.config.js
配置在项目根目录添加 webpack.config.js
文件, 添加如下配置
const path = require('path');module.exports = { target:'web', framework: 'vue', // 指定用 easywebpack-vue 解决方案, 请在项目中安装该依赖 entry: { index: 'src/index.js' }};
easy build deveasy build testeasy build prod
easy dev# 构建文件并启动本地静态 HTTP Servereasy build --server
运行完成自动打开编译结果页面 : http://127.0.0.1:8888/debug
easywebpack-cli
插件npm i easywebpack-cli -g
easy init
命令初始化骨架项目, 根据提示选择对应的项目类型即可.// ${root}/package.json{ "devDependencies": { "babel-plugin-import": "^1.0.0" }}
import { Button } from 'antd';
官方文档: https://ant.design/docs/react/getting-started-cn
主题定制需要开启 webpack less 编译
// ${root}/package.json{ "devDependencies": { "less": "^2.7.2", "less-loader": "^4.1.0" }}
//${root}/webpack.config.jsconst path = require('path');const resolve = (filepath) => path.resolve(__dirname, filepath);module.exports = { loaders: { babel: { include: [resolve('app/web'), resolve('node_modules')] }, less: { include: [resolve('app/web'), resolve('node_modules')], options: { javascriptEnabled: true, modifyVars: { 'primary-color': 'red', 'link-color': '#1DA57A', 'border-radius-base': '2px' } } } }};
如果是 SSR 模式,需要按如下 env 配置;前端渲染模式可以不用 env 方式。 BABEL_ENV 使用,请参考 /egg-react/babel
{ "env":{ "node": { "presets": [ "react", ["env", { "modules": false, "targets": { "node": "current" } }] ], "plugins": [ "syntax-dynamic-import", "transform-object-rest-spread" ] }, "web": { "presets": [ "react", ["env", { "modules": false, "targets": { "browsers": ["last 2 versions", "safari >= 7"] } }] ], "plugins": [ "react-hot-loader/babel", "transform-object-assign", "syntax-dynamic-import", "transform-object-rest-spread", ["import", { "libraryName": "antd", "libraryDirectory": "lib", "style": true }] ] } }, "comments": false}
]]>$ npm i easywebpack-weex --save-dev
const weex = require('easywebpack-weex');// 获取 webpack weex 配置const webpackConfig = weex.getWeexWebpackConfig({ env: process.env.BUILD_ENV, // 支持 dev,test,local 模式 entry: { index: 'src/app.js' }});// 获取 webpack web 配置const webpackConfig = weex.getWebWebpackConfig({ entry: { index: 'src/app.js' }});// 获取 webpack weex 和 web 配置const webpackConfig = weex.getWebpackConfig({ entry: { index: 'src/app.js' }});
webpack --config webpack.config.js
const weex = require('easywebpack-weex');if (process.env.NODE_ENV === 'development') { // development mode: webpack building and start webpack hot server weex.server(webpackConfig);} else { // build file to disk weex.build(webpackConfig);}
easywebpack-weex-boilerplate 项目骨架
import React from 'react';import ReactDom from 'react-dom';import { AppContainer } from 'react-hot-loader';import Entry from '${this.resourcePath.replace(/\\/g, '\\\\')}';const state = window.__INITIAL_STATE__;const render = (App)=>{ // 如果是 SSR 渲染时,请用 hydrate, 否则用 render,主要解决警告问题,不影响实际功能 ReactDom.hydrate(EASY_ENV_IS_DEV ? <AppContainer><App {...state} /></AppContainer> : <App {...state} />, document.getElementById('app'));};if (module.hot) { module.hot.accept('${this.resourcePath.replace(/\\/g, '\\\\')}', () => { render(Entry); });}render(Entry);
if (module.hot) { module.hot.accept('${this.resourcePath.replace(/\\/g, '\\\\')}', () => { render(Entry); });}改成if (module.hot) { module.hot.accept();}
https://github.com/easy-team/egg-react-webpack-boilerplate/issues
]]>在 前端渲染模式 章节讲到了基于 React 的一体化的前端渲染模式,好处是不需要借助第三方模板引擎且无效关注静态资源注入问题,但有两个小的功能限制:
layout 模板数据绑定能力较弱
资源注入不能自己定义,比如 async, crossorigin 等配置
针对上面问题 egg-view-react-ssr (>=2.4.0)扩展 renderAsset 方法支持基于 asset 的前端渲染模式,方便对 layout 进行定制和数据绑定。
// ${root}/package.json{ "dependencies": { "egg-view-nunjucks": "^2.2.0", "egg-view-react-ssr": "^2.4.0" }}
// ${root}/config/plugin.jsonexports.reactssr = { enable: true, package: 'egg-view-react-ssr'};exports.nunjucks = { enable: true, package: 'egg-view-nunjucks',};
模板默认路径:${root}/app/view/layout.html, 你可以通过 egg-view-react-ssr 的 layout 属性配置指定模板位置。
**渲染上下文暴露全局 asset 对象,参数如下:**
asset.js { Array } 页面依赖的静态 JS 资源 URL 列表, 来自 config/manifest.json
具体见 资源依赖
asset.css { Array } 页面依赖的静态 JS 资源 URL 列表,来自config/manifest.json
具体见 资源依赖
asset.state { Object } 页面渲染原始数据,用于 MV 框架初始化 state
以下就是基于 nunjucks 的语法的 layout 模板配置, 你可以根据指定渲染引擎编写 layout 文件。
<!DOCTYPE html><html lang='en'><head> <title>{{title}}</title> <meta name='keywords'> <meta name='description'> <meta http-equiv='content-type' content='text/html;charset=utf-8'> <meta name='viewport' content='initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui'> <link rel='shortcut icon' href='/favicon.ico' type='image/x-icon' /> {% for item in asset.css %} <link rel="stylesheet" href='{{item}}' /> {% endfor %}</head><body> <div id='app'></div> <script type="text/javascript"> window.__INITIAL_STATE__ = {{ asset.state | safe }}; </script> {% for item in asset.js %} <script type="text/javascript" src="{{item}}"></script> {% endfor %}</body></html>
const egg = require('egg');module.exports = class AppController extends egg.Controller { async home(ctx) { await ctx.renderAsset('app.js', { title: 'egg-react-asset' }); }}
- 可以通过 egg-view-react-ssr 的 viewEngine 配置全局渲染引擎
- 通过 renderAsset 的第三个参数 viewEngine 配置对应渲染引擎,只在当前渲染生效
const egg = require('egg');module.exports = class AdminController extends egg.Controller { async home(ctx) { // 使用 ejs 引擎,注意项目请安装 https://github.com/eggjs/egg-view-ejs 依赖 await ctx.renderAsset('admin.js', { title: 'egg-react-asset' }, { viewEngine: 'ejs' }); }}
https://github.com/easy-team/egg-react-webpack-boilerplate/tree/feature/green/asset
npm run dev
后你会看到如下界面, 启动了两个 Webpack 构建实例:Node 模式 和 Web 模式。SSR 运行需要 Webapck 单独构建 target: node
和 target: web
主要的差异在于 Webpack需要处理 require 机制以及磨平 Node 和 浏览器运行环境的差异。在 easywebpack
4.6.0 以下 SSR 版本构建方案实现时,Node 和 Web 模式采用的是一份 .babelrc
配置,这样导致构建的后代码全部变成 es5。 但 Node 现在LTS 版本已经是 8 了,而且 10 也在开发,不久将会发布。这样导致 Node 端构建的代码没有用到 ES6 的特性,我们期望根据 Node 版本构建指定 ES 模式代码,这样可以带来两个好处:
Node 端运行的 ES6 模块更好的执行效率
Node 端编译成 ES6,可以减小构建好的 JSBundle 文件大小和编译转换时间,同时带来更好的文件读取效率和执行效率。
.babelrc
配置{ "presets": [ "react", ["env", { "modules": false }] ], "plugins": [ "transform-object-assign", "syntax-dynamic-import", "transform-object-rest-spread", ["import", { "libraryName": "antd", "style": "css" }] ], "env": { "development": { "plugins": [ "react-hot-loader/babel" ] } }, "comments": false}
注意: 升级 babel 7 后,不支持如下 env 方式配置
关键措施: bable 本身支持通过 process.env.BABEL_ENV 加载 .babelrc 配置文件:
如果.babelrc** 配置了 `env.node` 或者 `env.web` 节点配置,easywebpack 底层将自动设置 **[process.env.BABEL_ENV](https://www.babeljs.cn/docs/usage/babelrc/)** 变量, 启动 BABEL ENV 编译机制。easywebpack 底层支持 **[process.env.BABEL_ENV](https://www.babeljs.cn/docs/usage/babelrc/)** 支持 node 和 web 的 env .babelrc**
** 节点配置。 另外关键的 target 配置:**
target.node
: Node 环境编译模式,可以是指定版本,比如配置:8.9.3,也可以配置当前运行的node版本:current。
target.browsers
: Web 浏览器编译模式,可以配置浏览器的版本等
{ "env":{ "node": { "presets": [ "react", ["env", { "modules": false, "targets": { "node": "current" } }] ], "plugins": [ "syntax-dynamic-import", "transform-object-rest-spread" ] }, "web": { "presets": [ "react", ["env", { "modules": false, "targets": { "browsers": ["last 2 versions", "safari >= 7"] } }] ], "plugins": [ "react-hot-loader/babel", "transform-object-assign", "syntax-dynamic-import", "transform-object-rest-spread", ["import", { "libraryName": "antd", "libraryDirectory": "lib", "style": true }] ] } }, "comments": false}
在 前端渲染模式 和 asset 渲染模式 章节讲到了基于 React 的前端渲染模式,但都依赖 egg-view-react-ssr 插件,那如何基于已有 egg 模板引擎 (egg-view-nunjucks 或 egg-view-ejs) + Webpack 完全自定义前端方案呢?
html-webpack-plugin
插件生成 HTML 文件,并自动注入 JS/CSS 依赖write-file-webpack-plugin
插件把 Webpack HTML 文件写到本地。Webpack 默认是在内存里面,无法直接读取。这里以 egg-view-nunjucks 为例,其它模板引擎类似。
npm install egg-view-nunjucks --save
npm install egg-webpack --save-dev
// ${root}/package.json{ "dependencies": { "egg-webpack": "^4.0.0", "egg-view-nunjucks": "^2.2.0", }}
// ${root}/config/plugin.local.jsexports.webpack = { enable: true, package: 'egg-webpack',};// ${root}/config/plugin.jsexports.nunjucks = { enable: true, package: 'egg-view-nunjucks',};
<!DOCTYPE html><html lang='en'><head> <title>{{title}}</title> <meta name='keywords'> <meta name='description'> <meta http-equiv='content-type' content='text/html;charset=utf-8'> <meta name='viewport' content='initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui'> <link rel='shortcut icon' href='/favicon.ico' type='image/x-icon' /> <!-- html-webpack-plugin 自动注入 css --></head><body> <div id='app'></div> <script type="text/javascript"> window.__INITIAL_STATE__ = {{ }}; </script> <!-- html-webpack-plugin 自动注入 js --></body></html>
// ${root}/config/local.jsmodule.exports = app => { const exports = {}; exports.webpack = { webpackConfigList: require('@easy-team/easywebpack-react').getWebpackConfig() }; return exports;}// ${root}/config/default.jsmodule.exports = app => { const exports = {}; exports.view = { defaultViewEngine: 'nunjucks', mapping: { '.tpl': 'nunjucks' }, }; return exports;}
const egg = require('egg');module.exports = class AppController extends egg.Controller { async home(ctx) { await ctx.render('app.tpl', { title: 'HTML渲染' }); }}
该配置基于 easywebpack 配置,如果要用原生 webpack 请参考:/blog/wumyiw
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = { egg: true, target: 'web', entry: { app: 'app/web/page/app/app.js' }, plugins: [ new HtmlWebpackPlugin({ chunks: ['runtime','common', 'app'], filename: '../view/app.tpl', template: './app/web/view/layout.tpl' }), new HtmlWebpackPlugin({ chunks: ['runtime','common', 'test'], filename: '../view/test.tpl', template: './app/web/view/layout.tpl' }), ]};
https://github.com/easy-team/egg-react-webpack-boilerplate/tree/feature/green/html
]]>安装脚手架 npm install easywebpack-cli -g
命令行,然后就可以使用 easy
命令
命令行运行 easy init
选择 egg + react server side render boilerplate 初始化骨架项目
安装依赖 npm install
git clone https://github.com/easy-team/egg-react-webpack-boilerplate.gitnpm install
初始化的项目提供多页面和SPA(react-router/react-redux)服务端渲染实例,可以直接运行。
https://marketplace.visualstudio.com/items?itemName=hubcarl.vscode-easy-plugin#overview
npm run dev
npm run dev 做了如下三件事情
启动 egg 应用
启动 Webpack 构建, 文件不落地磁盘,Webpack 构建的文件都在内存里面
构建会同时启动两个 Webpack 构建服务, 客户端js构建端口9000, 服务端端口9001
构建完成,Egg 应用正式可用,自动打开浏览器
npm run build 或 easy build
启动 Webpack 构建,文件落地磁盘
服务端构建的文件放到 app/view
目录
客户端构建的文件放到 public
目录
生成的 manifest.json
放到 config
目录
构建的文件都是 gitignore
的,部署时请注意把这些文件打包进去
启动应用前, 请设置 EGG_SERVER_ENV
环境变量,测试环境设置 test
, 正式环境设置 prod
npm start
通过项目根目录下的 webpack.config.js
配置文件构造出 Webpack 实际的配置文件
通过 egg-webpack 插件提供本地开发构建和热更新支持。SSR 模式时,egg-webpack 会启动两个 Webpack 构建服务, 客户端jsbundle构建,端口9000, 服务端jsbundle构建端口9001。
// config/config.local.js 本地 npm start 使用const easywebpack = require('easywebpack-react');exports.webpack = { webpackConfigList:easywebpack.getWebpackConfig()};
// ${root}/webpack.config.jsmodule.exports = { entry: { home: 'app/web/page/home/index.jsx' }};
React 项目代码放到 app/web 目录,页面入口目录为 page,该目录的 所有 .jsx 文件默认会作为 Webpack 的 entry 构建入口。建议每个页面目录的只保留一个.jsx 文件,jsx关联的组件可以放到widget 或者 component 目录。
支持多页面/单页面服务端渲染, 前端渲染, 静态页面三种方式.
在app/web/page 目录下面创建 home 目录 和 home.jsx 文件, Webpack 自动根据 .jsx 文件创建 entry入口, 具体实现请见webpack.config.js
import React, { Component } from 'react';import Header from 'component/layout/standard/header/header.jsx';import List from 'component/home/list.jsx';import './home.css';export default class Home extends Component { componentDidMount() { console.log('----componentDidMount-----'); } render() { return <div> <Header></Header> <div className="main"> <div className="page-container page-component"> <List list={this.props.list}></List> </div> </div> </div>; }
egg-view-react-ssr
插件 render
方法实现 Server Side Renderexports.index = function* (ctx) { yield ctx.render('home/home.js', Model.getPage(1, 10));};
app.get('/home', app.controller.home.home.index);
egg-view-react-ssr
插件 renderClient
方法实现 Client Side Renderexports.client = function* (ctx) { yield ctx.renderClient('home/home.js', Model.getPage(1, 10));};
app.get('/client', app.controller.home.home.client);
正式环境部署,请设置 EGG_SERVER_ENV=prod
环境变量, 更多请见运行环境
构建的 app/view
目录, public
目录以及 buildConfig.json
和 manifest.json
等文件, 都是 gitignore
的,部署时请注意把这些文件打包进去。
Webpack构建服务端(Node) JSBundle运行文件, 构建的服务端渲染模板文件位置 ${app_root}/app/view
Webpack构建浏览器JSBundle运行文件, 构建的前端资源(js/css/image)文件位置 ${app_root}/public
Webpack构建的 manifest.json
和 buildConfig.js
文件位置 ${app_root}/config
目录
easywebpack-cli 构建配置文件 webpack.config.js
放到项目根目录${app_root}/webpack.config.js
React代码文件${app_root}/app/web
下面, 主要包括 asset
, component
, framework
, page
, store
, view
等目录
├── asset // 资源文件 │ ├── css │ │ ├── global.css │ │ ├── normalize.css │ │ └── style.css │ ├── images │ │ ├── favicon.ico │ │ ├── loading.gif │ │ └── logo.png ├── component // jsx组件 │ ├── home │ │ └── list.jsx │ └── layout │ └── standard │ └── header │ ├── header.css │ └── header.jsx ├── framework │ └── entry │ ├── app.js │ └── loader.js ├── page // 页面目录, jsx结尾的的文件默认作为entry入口 │ ├── hello │ │ └── hello.jsx // 页面入口文件, 根据framework/entry/loader.js模板自动构建 │ └── home │ ├── home.css │ └── home.jsx └── view └── layout.jsx // layout模板文件, 提供统一html, header, body结构, page下面的jsx文件无需关心
├── app│ ├── controller│ │ ├── test│ │ │ └── test.js│ ├── extend│ ├── lib│ ├── middleware│ ├── mocks│ ├── proxy│ ├── router.js│ ├── view│ │ ├── home│ │ │ └── home.js // 服务器编译的jsbundle文件│ └── web // 前端工程目录│ ├── asset // 存放公共js,css资源│ ├── framework // 前端公共库和第三方库│ │ └── entry │ │ ├── loader.js // 根据jsx文件自动生成entry入口文件loader│ ├── page // 前端页面和webpack构建目录, 也就是webpack打包配置entryDir│ │ ├── home // 每个页面遵循目录名, js文件名, scss文件名, jsx文件名相同│ │ │ ├── home.scss│ │ │ ├── index.jsx│ └── component // 遵循目录名, js文件名, scss文件名, jsx 文件名相同│ ├── loading│ │ ├── loading.scss│ │ └── loading.jsx│ ├── test│ │ ├── test.jsx│ │ └── test.scss│ └── toast│ ├── toast.scss│ └── toast.jsx├── config│ ├── config.default.js│ ├── config.local.js│ ├── config.prod.js│ ├── config.test.js│ └── plugin.js├── doc├── index.js├── webpack.config.js // easywebpack-cli 构建配置├── public // webpack编译目录结构, render文件查找目录│ ├── static│ │ ├── css│ │ │ ├── home│ │ │ │ ├── home.07012d33.css│ │ │ └── test│ │ │ ├── test.4bbb32ce.css│ │ ├── img│ │ │ ├── change_top.4735c57.png│ │ │ └── intro.0e66266.png│ ├── test│ │ └── test.js│ └── vendor.js // 生成的公共打包库
egg-react-webpack-boilerplate基于easywebpack-react和egg-view-react(ssr)插件的工程骨架项目
easywebpack-react Webpack React 构建工程化方案.
easywebpack-cli Webpack 构建工程化脚手架.
egg-view-react-ssr react ssr 解决方案.
egg-webpack 本地开发热更新使用.
egg-webpack-react 本地开发渲染内存读取辅助插件
在 egg-react-webpack-boilerplate 骨架项目中, 提供了一些demo, 如果要进行新项目开发,可以删除部分文件:
app/web/page 是页面目录。下面的每个目录都是一个单独的页面,其中 spa 目录是一个单页面服务端渲染例子,其他是简单的 React 服务端渲染例子, 这些文件都可以删除,删除后,你需要自己按照类似方式添加页面进行开发。
app/controller 是服务端页面处理逻辑入口,下面都是例子,可以删除, 然后自己根据业务添加对应的controller
asset 是几个公共的静态资源文件,如果 app/web/component下面没有引用到,可以根据需要删除
controller 和 page 删除了部分文件后,需要清理 app/router.js 和 webpack.config.js 下面文件不存在的一下配置
app/web/component 下面的 app 是单页面的 router 配置,如果 app/web/page/app 删除了,这个也可以删除
app/web/component/layout 提供了单页面 layout 和 多页面 layout, 自己根据需要选用。
egg-react-webpack-boilerplate 项目单独提供了两个纯净版本分支用于实际项目开发
Egg2 + React 多页面服务端渲染分支 feature/green/multi
Egg2 + React + React Router + Redux + React-Redux 单页面服务端渲染分支 feature/green/spa
一般我们推荐把 easy build dev
, easy build test
, easy build prod
配置到 项目的 package.json
的 script 中去, 然后通过 npm run [command] 的方式使用。
通过 npm run [command]
方式使用 easy 命令时,不需要全局安装 easywepback-cli
命令行工具, 只需要把 easywepback-cli
安装到项目 devDependencies
即可。
在命令行直接使用 easy
命令时,需要全局安装 easywepback-cli
命令行工具。如果不安装, 可以通过 npm5 支持的 npx easy
方式运行。
{ "scripts": { "clean": "easy clean", "debug": "egg-bin debug", "build": "cross-env easy clean && easy build prod", "dev": "egg-bin dev", "start": "egg-scripts start", }}
本项目本地开发过程中, npm run dev
自动启动 Webpack 内存构建,无需手动构建;
测试环境和正式环境部署一定要先进行 npm run build
模式构建,然后再打包, 只用用 npm start
启动
如果不是用 egg-scripts start
启动应用, 请配置EGG_SERVER_ENV 环境变量。EGG_SERVER_ENV
表示 Egg 用那种方式启动, test
表示读取 config.test.js
配置, prod
表示读取 config.prod.js
配置, 线上运行一定要用 prod
模式。例如自己写 index.js
启动脚本, 然后通过 node index.js
启动时,请配置 EGG_SERVER_ENV 环境变量。
// ${root}/index.jsrequire('egg').startCluster({ baseDir: __dirname, port: process.env.PORT});
npm run dev
使用 egg-webpack
插件进行前端资源构建, 这个插件只会在本地开发启用。
easy build testnpm start
npm run buildnpm start
项目开发完成以后,我们要部署上线, 一般如下步骤:
npm run clean
npm run build
这里需要你自己实现把构建好的文件和项目问题一起打成 zip 或 tar 包,然后上传到部署平台进行部署。参数配置请见:/easywebpack/build
需要把构建后的文件(public目录,app/view 目录, config/manifest.json)与项目源代码一起打包部署,当然部分文件(README.md, eslint, gitignore等)可以不打进去。
如果 node_modules
在打包时也打进去,packjson.json 里面的 devDependencies 依赖是不需要打进去的,这些只在开发期间和 Webpack 构建期间用到,不需要打进去。如果打进去也没有问题,只是包非常大,部署上传是个问题。
如果 node_modules
在打包时不打进去,在启动之前,你需要先按照依赖 npm install --production
这里会把代码,构建文件,node_modules 以及 node 一起压缩程 zip, 这样线上在启动时就不需要按照依赖。
easy clean alleasy build prodeasy zip --deps --nodejs
这里仅仅把代码,构建文件一起压缩程 zip, 这样线上在启动时需要运行 npm install –production 按照依赖。
easy clean alleasy build prodeasy zip
npm start
如果不是用 egg-scripts start
启动应用, 请配置 EGG_SERVER_ENV 环境变量 EGG_SERVER_ENV=prod NODE_ENV=production
配置环境变量
安装 Node LST (8.x.x) 环境: https://nodejs.org/zh-cn
https://github.com/eggjs/egg-init/blob/master/README.zh-CN.md
npm i egg-init -gegg-init
选择 Simple egg app boilerplate
project 初始化 egg 项目
新建 ${app_root}/app/view
目录(egg view规范目录),并添加 .gitkeep
文件,保证该空目录被 git 提交到仓库
新建 ${app_root}/app/view/layout.html
文件,用于服务端渲染失败后,采用客户端渲染
<!DOCTYPE html><html lang="en"><head> <title>Egg + React + Webpack</title> <meta name="keywords"> <meta name="description"> <meta http-equiv="content-type" content="text/html;charset=utf-8"> <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" /></head><body> <div id="app"></div></body></html>
react 没有内置在 egg-view-react-ssr 里面, 项目需要显示安装依赖。
npm i react react-dom axios egg-view-react-ssr egg-scripts --save
npm i egg-bin cross-env easywebpack-cli easywebpack-react egg-webpack egg-webpack-react --save-dev
npm install
${app_root}/config/plugin.local.js
配置exports.webpack = { enable: true, package: 'egg-webpack'};exports.webpackreact = { enable: true, package: 'egg-webpack-react'};
${app_root}/config/plugin.js
配置exports.reactssr = { enable: true, package: 'egg-view-react-ssr'};
${app_root}/config/config.default.js
配置'use strict';const path = require('path');module.exports = app => { const config = exports = {}; // 保证构建的静态资源文件能够被访问到 config.static = { prefix: '/public/', dir: path.join(app.baseDir, 'public') }; config.reactssr = { renderOptions: { basedir: path.join(app.baseDir, 'app/view') } }; return config;}
easywebpack-cli
配置文件 ${app_root}/webpack.config.js
关于 entry 配置,请务必先看这篇文档:/egg-react/config
module.exports = { entry: { app: 'app/web/page/home/index.jsx' }};
${app_root}/.babelrc
文件{ "presets": [ "react", ["env", { "modules": false }] ], "plugins": [ "transform-object-assign", "syntax-dynamic-import", "transform-object-rest-spread", ["import", { "libraryName": "antd", "style": "css" }] ], "env": { "development": { "plugins": [ "react-hot-loader/babel" ] } }, "comments": false}
安装 babel 相关依赖
npm i babel-core babel-loader --save-dev
npm i babel-preset-env babel-plugin-syntax-dynamic-import babel-plugin-transform-object-assign babel-plugin-transform-object-rest-spread --save-dev
${app_root}/postcss.config.js
文件(非必须)module.exports = { plugins: [ require('autoprefixer') ]};
安装 autoprefixer 依赖
npm i autoprefixer --save-dev
${app_root}/.gitignore
配置.DS_Store.happypack/node_modules/npm-debug.log.idea/diststaticpublicprivaterun*.iml*tmp_sitelogs.vscodeconfig/manifest.jsonapp/view/*!app/view/layout.html!app/view/.gitkeeppackage-lock.json
${app_root}/app/web/page/home/index.jsx
页面文件import React, { Component } from 'react';import Layout from 'component/layout.jsx';import List from './componets/list';import './index.css';export default class HomeIndex extends Component { componentDidMount() { console.log('----componentDidMount-----'); } render() { return <Layout> <div className="main"> <div className="page-container page-component"> <List list={this.props.message}></List> </div> </div> </Layout>; }}
通过 egg-view-react-ssr
插件 render
方法实现, 请看服务端渲染和前端渲染模式
${app_root}/app/controller/home.js
module.exports = app => { return class HomeController extends app.Controller { async server() { const { ctx } = this; await ctx.render('home/index.js', { message: 'egg react server side render' }); } async client() { const { ctx } = this; /* - renderClient 前端渲染,Node层只做 layout.html和资源依赖组装,渲染交给前端渲染。 - 与服务端渲染的差别你可以通过查看运行后页面源代码即可明白两者之间的差异 */ await ctx.renderClient('home/index.js', { message: 'egg react client render' }); } };};
app.get('/', app.controller.home.server);app.get('/client', app.controller.home.client);
npm run dev
npm run dev 做了如下三件事情
首先启动 egg 应用
启动 webpack(egg-webpack) 构建, 文件不落地磁盘,构建的文件都在内存里面(只在本地启动, 发布模式是提前构建好文件到磁盘)
构建会同时启动两个 Webpack 构建服务, 客户端js构建端口9000, 服务端端口9001
构建完成,Egg 应用正式可用,自动打开浏览器
${app_root}/package.json
添加命令{ "scripts": { "dev": "egg-bin dev", "start": "egg-scripts start", "debug": "egg-bin debug", "clean": "easy clean all", "build": "easy build", },}
npm run build 或 easy build prod
启动 Webpack 构建,文件落地磁盘
服务端构建的文件放到 app/view
目录
客户端构建的文件放到 public
目录
生成的 manifest.json
放到 config
目录
构建的文件都是gitignore的,部署时请注意把这些文件打包进去
启动应用前, 如果是非 egg-scripts
方式启动, 请设置 EGG_SERVER_ENV
环境变量,本地local, 测试环境设置 test
, 正式环境设置 prod
npm start
egg-react-webpack-boilerplate 基于easywebpack-react和 egg-view-react-ssr插件的工程骨架项目
easywebpack-react Webpack React 构建工程化基础
easywebpack-cli Webpack 构建工程化脚手架.
egg-view-react-ssr egg react ssr 插件.
egg-webpack 本地开发热更新使用.
egg-webpack-react 本地开发渲染内存读取辅助 egg-webpack-react插件
以上详细步骤只是告诉大家 Egg + React + easywebpack 搭建项目整个流程,帮助搭建理清流程和细节。实际使用使用时建议使用 easywebpack-cli 初始化项目或者 clone egg-react-webpack-boilerplate 代码初始化项目。
]]>https://github.com/easy-team/egg-react-webpack-boilerplate/blob/master/CHANGELOG.md
骨架分支: master,版本 4.6.0
骨架分支: feature/green/html
see issue:https://github.com/easy-team/egg-vue-webpack-boilerplate/issues/125
app.$mount('app');改为const root = document.getElementById('app');const hydrate = root.childNodes.length > 0;app.$mount(root, hydrate);
在 Egg + Vue/React 解决方案中, Webpack publicPath 使用的是默认 publicPath: '/public/'
配置。
如果要修复默认的 publicPath,比如要修改 /static/
,需要修改两个地方:
easywebpack: ^3.5.1
egg-webpack: ^3.2.5
webpack.config.js
配置添加 publicPath
配置覆盖默认配置// ${app_root}/webpack.config.js module.exports = { ..... publicPath: '/static/' };
config.default.js
添加静态资源// ${app_root}/config/config.local.js exports.static = { prefix: '/static/', dir: path.join(app.baseDir, 'public') };
// ${app_root}/config/config.local.js exports.webpack = { proxy: { match:/\/static\// } };
see https://github.com/easy-team/egg-vue-webpack-boilerplate/issues/80
// ${app_root}/config/config.local.jsexports.webpack = { browser: false};
通过 egg-webpack 实现该功能,详细可以看插件具体文档。
// ${app_root}/config/config.local.jsexports.webpack = { browser: 'http://localhost:7001'};
通过 egg-webpack 实现该功能,详细可以看插件具体文档。
ReferenceError: document is not defined
https://zhuanlan.zhihu.com/p/36233639
在 前端渲染模式 章节讲到了基于 Vue 的一体化的前端渲染模式,好处是不需要借助第三方模板引擎且无需关注静态资源注入问题,但有两个小的功能限制:
layout 模板数据绑定能力较弱
资源注入不能自己定义,比如 async, crossorigin 等配置
针对上面问题 egg-view-vue-ssr (>=3.2.0)扩展 renderAsset 方法支持基于 asset 的前端渲染模式,方便对 layout 进行定制和数据绑定。
// ${root}/package.json{ "dependencies": { "egg-view-nunjucks": "^2.2.0", "egg-view-vue-ssr": "^3.2.0" }}
// ${root}/config/plugin.jsexports.vuessr = { enable: true, package: 'egg-view-vue-ssr'};exports.nunjucks = { enable: true, package: 'egg-view-nunjucks',};
模板默认路径:${root}/app/view/layout.html, 你可以通过 egg-view-vue-ssr 的 layout 属性配置指定模板位置。
渲染上下文暴露全局 asset 对象,参数如下:
asset.js { Array } 页面依赖的静态 JS 资源 URL 列表, 来自 config/manifest.json
具体见 资源依赖
asset.css { Array } 页面依赖的静态 JS 资源 URL 列表,来自config/manifest.json
具体见 资源依赖
asset.state { Object } 页面渲染原始数据,用于 MV 框架初始化 state
以下就是基于 nunjucks 的语法的 layout 模板配置, 你可以根据指定渲染引擎编写 layout 文件。
<!DOCTYPE html><html lang='en'><head> <title>{{title}}</title> <meta name='keywords'> <meta name='description'> <meta http-equiv='content-type' content='text/html;charset=utf-8'> <meta name='viewport' content='initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui'> <link rel='shortcut icon' href='/favicon.ico' type='image/x-icon' /> {% for item in asset.css %} <link rel="stylesheet" href='{{item}}' /> {% endfor %}</head><body> <div id='app'></div> <script type="text/javascript"> window.__INITIAL_STATE__ = {{ asset.state | safe }}; </script> {% for item in asset.js %} <script type="text/javascript" src="{{item}}"></script> {% endfor %}</body></html>
const egg = require('egg');module.exports = class AdminController extends egg.Controller { async home(ctx) { await ctx.renderAsset('admin.js', { title: 'egg-vue-asset' }); }}
- 可以通过 egg-view-vue-ssr 的 viewEngine 配置全局渲染引擎
- 通过 renderAsset 的第三个参数 viewEngine 配置对应渲染引擎,只在当前渲染生效
const egg = require('egg');module.exports = class AdminController extends egg.Controller { async home(ctx) { // 使用 ejs 引擎,注意项目请安装 https://github.com/eggjs/egg-view-ejs 依赖 await ctx.renderAsset('admin.js', { title: 'egg-vue-asset' }, { viewEngine: 'ejs' }); }}
https://github.com/easy-team/egg-vue-webpack-boilerplate/tree/feature/green/asset
Webpack打包是把所有js代码打成一个js文件,我们可以通过 CommonsChunkPlugin
分离出公共组件,但这远远不够。 实际业务开发时,一些主要页面内容往往比较多, 而且会引入第三方组件或者监控脚本。其中有些内容的展示不再首屏或者监控脚本等对用户不是那么重要的脚本我们可以通过 require.ensure
代码分离延迟加载。在webpack在构建时,解析到require.ensure
时,会单独针对引入的js资源单独构建出chunk文件,这样就能从主js文件里面分离出来。 然后页面加载完后, 通过script标签的方式动态插入到文档中。
require.ensure 使用方式, 第三个参数是指定生产的 chunk 文件名,不设置时是用数字编号代理。相同 require.ensure 只会生产一个chunk文件。
require.ensure(['swiper'], ()=> { const Swiper = require('swiper'); ...... }, 'swiper');
异步加载 Vue 组件(.vue) 已在 Vue 2.5+ 版本支持,包括路由异步加载和非路由异步加载。在具体实现时,我们可以通过 import(filepath)
加载组件。
import()
方案已经列入 ECMAScript提案,虽然在提案阶段,但 Webpack 已经支持了该特性。import() 返回的 Promise,通过注释 webpackChunkName 指定生成的 chunk 名称。 Webpack 构建时会独立的 chunkjs 文件,然后在客户端动态插入组件,chunk 机制与 require.ensure 一样。有了动态加载的方案,可以减少服务端渲染 jsbundle 文件的大小,页面 Vue 组件模块也可以按需加载。
Vue.component('async-swiper', (resolve) => { // 通过注释webpackChunkName 指定生成的chunk名称 import(/* webpackChunkName: "asyncSwiper" */ './AsyncSwiper.js') .then((AsyncSwiper) => { resolve(AsyncSwiper.default); });});<div id="app"> <p>Vue dynamic component load</p> <async-swiper></async-swiper></div>
构建适配 vue-server-renderer
异步渲染查找 chunk
文件逻辑。这里直接把 chunk
文件构建到 app/view/node_modules
下面, 这样异步渲染才能找到该文件。
const path = require('path');const fs = require('fs');module.exports = app => { const exports = {}; exports.vuessr = { renderOptions: { // 告诉 vue-server-renderer 去 app/view 查找异步 chunk 文件 basedir: path.join(app.baseDir, 'app/view') } }; return exports;};
app/web/page/dynamic/dynamic.vue
npm run dev
后你会看到如下界面, 启动了两个 Webpack 构建实例:Node 模式 和 Web 模式。SSR 运行需要 Webapck 单独构建 target: node
和 target: web
主要的差异在于 Webpack需要处理 require 机制以及磨平 Node 和 浏览器运行环境的差异。在 easywebpack
4.6.0 以下 SSR 版本构建方案实现时,Node 和 Web 模式采用的是一份 .babelrc
配置,这样导致构建的后代码全部变成 es5。 但 Node 现在LTS 版本已经是 8 了,而且 10 也在开发,不久将会发布。这样导致 Node 端构建的代码没有用到 ES6 的特性,我们期望根据 Node 版本构建指定 ES 模式代码,这样可以带来两个好处:
.babelrc
配置{ "presets": [["env",{ "modules": false }]], "plugins": [ "transform-object-rest-spread", "syntax-dynamic-import", "transform-object-assign" ], "comments": false}
Node 端运行的 ES6 模块更好的执行效率
Node 端编译成 ES6,可以减小构建好的 JSBundle 文件大小和编译转换时间,同时带来更好的文件读取效率和执行效率。
注意: 升级 babel 7 后,不支持如下 env 方式配置
关键措施: bable 本身支持通过 process.env.BABEL_ENV 加载 .babelrc 配置文件:
如果.babelrc** 配置了 `env.node` 或者 `env.web` 节点配置,easywebpack 底层将自动设置 **[process.env.BABEL_ENV](https://www.babeljs.cn/docs/usage/babelrc/)** 变量, 启动 BABEL ENV 编译机制。easywebpack 底层支持 **[process.env.BABEL_ENV](https://www.babeljs.cn/docs/usage/babelrc/)** 支持 node 和 web 的 env .babelrc**
** 节点配置。 另外关键的 target 配置:**
target.node
: Node 环境编译模式,可以是指定版本,比如配置:8.9.3,也可以配置当前运行的node版本:current。
target.browsers
: Web 浏览器编译模式,可以配置浏览器的版本等
{ "env":{ "node": { "presets": [["env", { "modules": false, "targets": { "node": "current" } }]], "plugins": [ "transform-object-rest-spread", "syntax-dynamic-import" ] }, "web": { "presets": [["env", { "modules": false, "targets": { "browsers": ["last 2 versions", "safari >= 7"] } }]], "plugins": [ "transform-object-rest-spread", "syntax-dynamic-import", "transform-object-assign" ] } }, "comments":false}
合理的使用 BABEL 编译模式,能够极大提高构建速度和JS 文件大小。 通过测试,启用 BABEL_ENV 模式和合理的配置 targets.browsers 参数,对于大型的页面,能够显著提升构建速度。下面通过 easy build prod
针对 https://github.com/hubcarl/egg-vue-webpack-boilerplate 测试的效果,页面比较简单,效果不明显。
模式 | 构建大小(app/app.js) |
---|---|
不启用BABEL按需编译 | 15.6 KiB |
启用BABEL按需编译 | 15.2 KiB |
easywebpack@4.8.0 开始支持,因为有了默认配置,所以最新的骨架项目中,webpack.config.js 文件为非必须配置。
使用 node-glob 遍历文件。下面配置会自动遍历 app/web/page
目录的所有 .vue 文件作为 entry 入口,排除 component|components|view|views
目录下的文件。 这个是 egg vue ssr 项目默认配置, 同时使用 vue-entry-loader 作为模板入口。 注意:只有 entry 文件是 .vue 文件(非.js)时,才会自动使用 **vue-entry-loader 模板。**
// webpack.config.jsmodule.exports = { // 注意 只有 entry 文件是 .vue 文件(非.js)时,才会自动使用 vue-entry-loader模板 entry: 'app/web/page/**!(component|components|view|views)/*.vue'}
module.exports = { entry: { app: 'app/web/page/app/index.js', // js 文件需要自己实现 vue mouted 逻辑 list: 'app/web/page/list/index.vue' // 自动使用 vue-entry-loader模板 }};
npm install vue-i18n --save
export default { menu: { server: '服务端渲染', client: '前端渲染', dynamic: '动态渲染', element: 'Element', single: '单页面', }, lang: { href: '/?locale=en', text: '切换英文版' }};
export default { menu: { server: 'Server', client: 'Client', dynamic: 'Dynamic', element: 'Element', single: 'SPA', }, lang: { href: '/?locale=cn', text: 'Switch Chinese' }};
import VueI18n from 'vue-i18n';import cn from './cn';import en from './en';export default function createI18n(locale) { return new VueI18n({ locale, messages: { en, cn } });}
export default function render(options) { return context => { // locale 是从 Node 端传递过来的配置参数,用于加载指定语言文件 const i18n = createI18n(context.state.locale); const VueApp = Vue.extend(options); const app = new VueApp({ data: context.state, i18n }); return new Promise(resolve => { resolve(app); }); };}
new Vue({ i18n }).$mount('#app')
ctx.locals
(egg-view-vue-ssr 渲染时,会统一合并 locals)//${root}/app/middleware/locals.jsmodule.exports = () => { return async function locale(ctx, next) { ctx.locals.locale = ctx.query.locale || 'cn'; await next(); };};
//${root}/config/config.default.jsexports.middleware = [ 'locals'];
在 前端渲染模式 和 asset 渲染模式 章节讲到了基于 React 的前端渲染模式,但都依赖 egg-view-react-ssr 插件,那如何基于已有 egg 模板引擎 (egg-view-nunjucks 或 egg-view-ejs) + Webpack 完全自定义前端方案呢?
html-webpack-plugin
插件生成 HTML 文件,并自动注入 JS/CSS 依赖write-file-webpack-plugin
插件把 Webpack HTML 文件写到本地。Webpack 默认是在内存里面,无法直接读取。这里以 egg-view-nunjucks 为例,其它模板引擎类似。
npm install egg-view-nunjucks --save
npm install egg-webpack --save-dev
// ${root}/package.json{ "dependencies": { "egg-webpack": "^4.0.0", "egg-view-nunjucks": "^2.2.0", }}
// ${root}/config/plugin.local.jsexports.webpack = { enable: true, package: 'egg-webpack',};// ${root}/config/plugin.jsexports.nunjucks = { enable: true, package: 'egg-view-nunjucks',};
<!DOCTYPE html><html lang='en'><head> <title>{{title}}</title> <meta name='keywords'> <meta name='description'> <meta http-equiv='content-type' content='text/html;charset=utf-8'> <meta name='viewport' content='initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui'> <link rel='shortcut icon' href='/favicon.ico' type='image/x-icon' /> <!-- html-webpack-plugin 自动注入 css --></head><body> <div id='app'></div> <script type="text/javascript"> window.__INITIAL_STATE__ = {{ }}; </script> <!-- html-webpack-plugin 自动注入 js --></body></html>
// ${root}/config/local.jsmodule.exports = app => { const exports = {}; exports.webpack = { webpackConfigList: require('easywebpack-react').getWebpackConfig() }; return exports;}// ${root}/config/default.jsmodule.exports = app => { const exports = {}; exports.view = { defaultViewEngine: 'nunjucks', mapping: { '.tpl': 'nunjucks' }, }; return exports;}
const egg = require('egg');module.exports = class AppController extends egg.Controller { async home(ctx) { await ctx.render('layout.tpl', { title: '自定义渲染' }); }}
该配置基于 easywebpack 配置,如果要用原生 webpack 请参考:/blog/wumyiw
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = { egg: true, target: 'web', entry: { app: 'app/web/page/app/app.js' }, plugins: [ new HtmlWebpackPlugin({ chunks: ['runtime','common', 'app'], filename: '../view/app.tpl', template: './app/web/view/layout.tpl' }), new HtmlWebpackPlugin({ chunks: ['runtime','common', 'test'], filename: '../view/test.tpl', template: './app/web/view/layout.tpl' }), ]};
https://github.com/easy-team/egg-react-webpack-boilerplate/tree/feature/green/html
]]>在 前端渲染模式 和 asset 渲染模式 章节讲到了基于 React 的前端渲染模式,但都依赖 egg-view-react-ssr 插件,那如何基于已有 egg 模板引擎 (egg-view-nunjucks 或 egg-view-ejs) + Webpack 完全自定义前端方案呢?
html-webpack-plugin
插件生成 HTML 文件,并自动注入 JS/CSS 依赖write-file-webpack-plugin
插件把 Webpack HTML 文件写到本地。Webpack 默认是在内存里面,无法直接读取。这里以 egg-view-nunjucks 为例,其它模板引擎类似。
npm install egg-view-nunjucks --save
npm install egg-webpack --save-dev
// ${root}/package.json{ "dependencies": { "egg-webpack": "^4.0.0", "egg-view-nunjucks": "^2.2.0", }}
// ${root}/config/plugin.local.jsexports.webpack = { enable: true, package: 'egg-webpack',};// ${root}/config/plugin.jsexports.nunjucks = { enable: true, package: 'egg-view-nunjucks',};
<!DOCTYPE html><html lang='en'><head> <title>{{title}}</title> <meta name='keywords'> <meta name='description'> <meta http-equiv='content-type' content='text/html;charset=utf-8'> <meta name='viewport' content='initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui'> <link rel='shortcut icon' href='/favicon.ico' type='image/x-icon' /> <!-- html-webpack-plugin 自动注入 css --></head><body> <div id='app'></div> <script type="text/javascript"> window.__INITIAL_STATE__ = {{ }}; </script> <!-- html-webpack-plugin 自动注入 js --></body></html>
// ${root}/config/local.jsmodule.exports = app => { const exports = {}; exports.webpack = { webpackConfigList: require('@easy-team/easywebpack-vue').getWebpackConfig() }; return exports;}// ${root}/config/default.jsmodule.exports = app => { const exports = {}; exports.view = { defaultViewEngine: 'nunjucks', mapping: { '.tpl': 'nunjucks' }, }; return exports;}
const egg = require('egg');module.exports = class AppController extends egg.Controller { async home(ctx) { await ctx.render('app.tpl', { title: '自定义渲染' }); }}
该配置基于 easywebpack 配置,如果要用原生 webpack 请参考:/blog/wumyiw
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = { target: 'web', entry: { app: 'app/web/page/app/app.js' }, plugins: [ new HtmlWebpackPlugin({ chunks: ['runtime','common', 'app'], filename: '../view/app.tpl', template: './app/web/view/layout.tpl' }), new HtmlWebpackPlugin({ chunks: ['runtime','common', 'test'], filename: '../view/test.tpl', template: './app/web/view/layout.tpl' }), ]};
https://github.com/easy-team/egg-vue-webpack-boilerplate/tree/feature/green/html
]]>安装脚手架 npm install easywebpack-cli -g
命令行,然后就可以使用 easywebpack
或 easy
命令
命令行运行 easywebpack init
选择 egg+vue server side render boilerplate 初始化骨架项目
安装依赖 npm install
git clone https://github.com/hubcarl/egg-vue-webpack-boilerplate.gitnpm install
初始化的项目提供多页面和SPA(vue-router/axios)服务端渲染实例,可以直接运行。
https://marketplace.visualstudio.com/items?itemName=hubcarl.vscode-easy-plugin#overview
npm run dev // egg-bin dev
npm run dev 做了如下三件事情
启动 egg 应用
启动 Webpack 构建, 文件不落地磁盘,构建的文件都在内存里面(只在本地启动, 发布模式是提前构建好文件到磁盘)
构建会同时启动两个 Webpack 构建服务, 客户端js构建端口9000, 服务端端口9001
构建完成,Egg应用正式可用,自动打开浏览器
npm run build 或 easy build prod
启动 Webpack 构建,文件落地磁盘
服务端构建的文件放到 app/view
目录
前端构建的文件放到 public
目录
生成的 manifest.json
放到 config
目录
构建的文件都是 gitignore 的,部署时请注意把这些文件打包进去
非
egg-scripts start
方式启动时, 启动应用前, 请设置EGG_SERVER_ENV
环境变量,测试环境设置test
, 正式环境设置prod
npm start // egg-scripts start
通过 easywebpack-cli
统一构建,支持 dev,test,prod 模式构建
easywebpack-cli
通过项目根目录下的 webpack.config.js
配置文件构造出 Webpack 实际的配置文件,配置项请见 webpack.config.js
获取 Webpack 实际的配置文件, egg-webpack 会使用到该功能。构建会根据 webpackConfigList.length
启动对应个数的 Webpack 编译实例,这里会同时启动两个 Webpack 构建服务, 客户端jsbundle构建,端口9000, 服务端jsbundle构建端口9001。默认端口为9000, 端口依次递增。
// config/config.local.js 本地 npm start 使用const EasyWebpack = require('easywebpack-vue');exports.webpack = { webpackConfigList:EasyWebpack.getWebpackConfig()};
app/web/page
目录中所有 .vue 文件当作 Webpack 构建入口是采用 app/web/framework/vue/entry 的 client-loader.js 和 *server-loader.js *模板实现的,这个需要结合 webpack.config.js
下的 entry.loader 使用。 骨架最新版本已被下面 node-glob 模式所替换。 这种方式可以自定义 entry 初始化模板。entry: { include: ['app/web/page'], exclude: ['app/web/page/[a-z]+/component', 'app/web/page/app'], loader: { // 如果没有配置loader模板,默认使用 .js 文件作为构建入口 client: 'app/web/framework/vue/entry/client-loader.js', server: 'app/web/framework/vue/entry/server-loader.js', } }
app/web/page
目录的所有 vue 文件作为 entry 入口,排除 component|components|view|views
目录下的文件。 这个是 egg vue ssr 项目默认配置, 同时使用 vue-entry-loader 作为模板入口 。easywebpack@4.8.0 开始支持,因为有了默认配置,所以最新的骨架项目中,webpack.config.js 文件为非必须配置。// webpack.config.jsmodule.exports = { // 注意 只有 entry 文件是 .vue 文件(非.js)时,才会自动使用 vue-entry-loader 模板 entry: 'app/web/page/**!(component|components|view|views)/*.vue'}
Vue 项目代码放到 app/web 目录,页面入口目录为 page,该目录的 所有 vue 文件默认会作为 Webpack 的 entry 构建入口。建议每个页面目录的只保留一个vue文件,vue关联的组件可以放到widget 或者 compnent目录。如果非要放到当前目录,请配置 webpack.config.js
entry.exclude 排除 vue文件。
支持多页面/单页面服务端渲染, 前端渲染, 静态页面三种方式.
在app/web/page 目录下面创建home目录, home.vue 文件, Webpack自动根据.vue文件创建entry入口, 具体实现请见webpack.config.js
<template> <layout title="egg vue ssr" description="vue server side render" keywords="egg, vue, webpack, server side render"> {{message}} </layout></template><style> @import "home.css";</style><script type="text/babel"> export default { components: { }, computed: { }, methods: { }, mounted() { } }</script>
egg-view-vue-ssr
插件 render
方法实现exports.index = function* (ctx) { yield ctx.render('home/home.js', { message: 'vue server side render!' });};
app.get('/home', app.controller.home.index);
egg-view-vue-ssr
插件 renderClient
方法实现exports.client = function* (ctx) { yield ctx.renderClient('home/home.js', { message: 'vue server side render!' });};
app.get('/client', app.controller.home.home.client);
直接有easywebpack构建出静态HTML文件, 请见 webpack.config.js
配置和 app/web/page/html
代码实现
通过 egg-static
静态文件访问HTML文件
在app/web/page 目录下面创建app目录, app.vue, app.js 文件.
<template> <app-layout> <transition name="fade" mode="out-in"> <router-view></router-view> </transition> </app-layout></template><style lang="sass"></style><script type="text/babel"> export default { computed: { }, mounted(){ } }</script>
import { sync } from 'vuex-router-sync';import store from 'store/app';import router from 'component/app/router';import app from './app.vue';import App from 'app';import Layout from 'component/layout/app';App.component(Layout.name, Layout);sync(store, router);export default App.init({ base: '/app', ...app, router, store});
exports.index = function* (ctx) { yield ctx.render('app/app.js', { url: this.url.replace(/\/app/, '') });};
app.get('/app(/.+)?', app.controller.app.app.index);
正式环境部署,请设置 EGG_SERVER_ENV=prod
环境变量, 更多请见运行环境
构建的 app/view
目录, public
目录以及 buildConfig.json
和 manifest.json
等文件, 都是 gitignore
的,部署时请注意把这些文件打包进去。
Webpack构建服务端(Node) JSBundle运行文件, 构建的服务端渲染模板文件位置 ${app_root}/app/view
Webpack构建浏览器JSBundle运行文件, 构建的前端资源(js/css/image)文件位置 ${app_root}/public
Webpack构建的 manifest.json
文件位置 ${app_root}/config
目录
easywebpack-cli 构建配置文件 webpack.config.js
放到项目根目录${app_root}/webpack.config.js
Vue代码文件${app_root}/app/web
下面, 主要包括 asset
, component
, framework
, page
, store
, view
等目录
├── asset│ ├── css│ │ ├── normalize.css│ │ └── style.css│ ├── images│ │ ├── favicon.ico│ │ ├── loading.gif│ │ └── logo.png├── component│ ├── app│ │ ├── detail.vue│ │ ├── list.vue│ │ └── router.js│ ├── layout│ │ ├── app│ │ │ ├── content│ │ │ │ ├── content.css│ │ │ │ └── content.vue│ │ │ ├── footer│ │ │ │ ├── footer.css│ │ │ │ └── footer.vue│ │ │ ├── header│ │ │ │ ├── header.css│ │ │ │ └── header.vue│ │ │ ├── index.js│ │ │ └── main.vue├── framework│ ├── inject│ │ ├── global.css│ │ ├── inline.js│ │ └── pack-inline.js│ └── vue│ ├── app.js│ ├── component│ │ └── index.js│ ├── directive│ │ └── index.js│ └── filter│ └── index.js├── page│ ├── app│ │ ├── app.js│ │ └── app.vue│ ├── index│ │ ├── index.css│ │ ├── index.js│ │ └── index.vue├── store│ └── app│ ├── actions.js│ ├── getters.js│ ├── index.js│ ├── mutation-type.js│ └── mutations.js└── view └── layout.html
├── app│ ├── controller│ │ ├── test│ │ │ └── test.js│ ├── extend│ ├── lib│ ├── middleware│ ├── mocks│ ├── proxy│ ├── router.js│ ├── view│ │ ├── about // 服务器编译的jsbundle文件│ │ │ └── about.js│ │ ├── home│ │ │ └── home.js // 服务器编译的jsbundle文件│ │ └── layout.js // 编译的layout文件│ └── web // 前端工程目录│ ├── asset // 存放公共js,css资源│ ├── framework // 前端公共库和第三方库│ │ └── entry │ │ ├── loader.js // 根据jsx文件自动生成entry入口文件loader│ ├── page // 前端页面和webpack构建目录, 也就是webpack打包配置entryDir│ │ ├── home // 每个页面遵循目录名, js文件名, scss文件名, jsx文件名相同│ │ │ ├── home.scss│ │ │ ├── home.jsx│ │ └── hello // 每个页面遵循目录名, js文件名, scss文件名, jsx文件名相同│ │ ├── test.css // 服务器render渲染时, 传入 render('test/test.js', data)│ │ └── test.jsx│ ├── store │ │ ├── app│ │ │ ├── actions.js│ │ │ ├── getters.js│ │ │ ├── index.js│ │ │ ├── mutation-type.js│ │ │ └── mutations.js│ │ └── store.js│ └── component // 公共业务组件, 比如loading, toast等, 遵循目录名, js文件名, scss文件名, jsx文件名相同│ ├── loading│ │ ├── loading.scss│ │ └── loading.jsx│ ├── test│ │ ├── test.jsx│ │ └── test.scss│ └── toast│ ├── toast.scss│ └── toast.jsx├── config│ ├── config.default.js│ ├── config.local.js│ ├── config.prod.js│ ├── config.test.js│ └── plugin.js├── doc├── index.js├── webpack.config.js // easywebpack-cli 构建配置├── public // webpack编译目录结构, render文件查找目录│ ├── static│ │ ├── css│ │ │ ├── home│ │ │ │ ├── home.07012d33.css│ │ │ └── test│ │ │ ├── test.4bbb32ce.css│ │ ├── img│ │ │ ├── change_top.4735c57.png│ │ │ └── intro.0e66266.png│ ├── test│ │ └── test.js│ └── vendor.js // 生成的公共打包库
egg-vue-webpack-boilerplate 基于easywebpack-vue和egg-view-vue(ssr)插件的工程骨架项目
easywebpack-vue Webpack Vue 构建工程化方案.
easywebpack-cli Webpack 构建工程化脚手架.
egg-view-vue-ssr egg vue ssr 解决方案.
egg-webpack 本地开发热更新使用.
egg-webpack-vue 本地开发渲染内存读取辅助插件
在 egg-vue-webpack-boilerplate 骨架项目中, 提供了一些demo, 如果要进行新项目开发,可以删除部分文件:
app/web/page 是页面目录。下面的每个目录都是一个单独的页面,其中 app 目录是一个单页面服务端渲染例子,其他是简单的 Vue 服务端渲染例子, 这些文件都可以删除,删除后,你需要自己按照类似方式添加页面进行开发。
app/controller 是服务端页面处理逻辑入口,下面都是例子,可以删除, 然后自己根据业务添加对应的controller
asset 是几个公共的静态资源文件,如果 app/web/component下面没有引用到,可以根据需要删除
controller 和 page 删除了部分文件后,需要清理 app/router.js 和 webpack.config.js 下面文件不存在的一下配置
app/web/component 下面的 app 是单页面的 router 配置,如果 app/web/page/app 删除了,这个也可以删除
app/web/component/layout 提供了单页面 layout 和 多页面 layout, 自己根据需要选用。
egg-vue-webpack-boilerplate 项目单独提供了两个纯净版本分支用于实际项目开发
Egg + Vue + axios 多页面服务端渲染分支 feature/green/multi
Egg + Vue + Vue-Router + Vuex + Axios 单页面服务端渲染分支 feature/green/spa
一般我们推荐把 easy build dev
, easy build test
, easy build prod
配置到 项目的 package.json
的 script 中去, 然后通过 npm run [command] 的方式使用。
通过 npm run [command]
方式使用 easy 命令时,不需要全局安装 easywepback-cli
命令行工具, 只需要把 easywepback-cli
安装到项目 devDependencies
即可。
在命令行直接使用 easy
命令时,需要全局安装 easywepback-cli
命令行工具。如果不安装, 可以通过 npm5 支持的 npx easy
方式运行。
{ "scripts": { "clean": "easy clean", "debug": "egg-bin debug", "build": "cross-env easy clean && easy build prod", "dev": "egg-bin dev", "start": "egg-scripts start", }}
本项目本地开发过程中, npm run dev
自动启动 Webpack 内存构建,无需手动构建;
测试环境和正式环境发布部署一定要先进行 npm run build 模式构建,然后再打包, 只用 npm start 启动即可
如果不是用 egg-scripts start
启动应用, 请配置 EGG_SERVER_ENV 环境变量。EGG_SERVER_ENV
表示 Egg 用那种方式启动, test
表示读取 config.test.js
配置, prod
表示读取 config.prod.js
配置, 正式线上运行一定要用 prod
模式。例如自己写 index.js
启动脚本, 然后通过 node index.js
启动时,请配置 EGG_SERVER_ENV 环境变量。
// ${root}/index.jsrequire('egg').startCluster({ baseDir: __dirname, port: process.env.PORT});
npm run dev
使用 egg-webpack
插件进行前端资源构建, 这个插件只会在本地开发启用。
npm run build:testnpm run start:test
npm run buildnpm start
项目开发完成以后,我们要部署上线, 一般如下步骤:
npm run clean
npm run build
这里需要你自己实现把构建好的文件和项目文件一起打成 zip 或 tar 包,然后上传到部署平台进行部署。参数配置请见:/easywebpack/build
Node 端项目源代码需要打包上传,包括 config, app 目录 以及app/web/view(app/web 目录除外)
需要把构建后的文件(public目录,app/view 目录, config/manifest.json)与 Node 端项目源代码一起打包部署,当然部分文件(README.md, eslint, gitignore等)可以不打进去。
如果 node_modules
在打包时也打进去,packjson.json 里面的 devDependencies 依赖是不需要打进去的,这些只在开发期间和 Webpack 构建期间用到,不需要打进去。如果打进去也没有问题,只是包非常大,部署上传是个问题。
如果 node_modules
在打包时不打进去,在启动之前,你需要先按照依赖 npm install --production
这里会把代码,构建文件,node_modules 以及 node 一起压缩程 zip, 这样线上在启动时就不需要安装依赖。
easy clean alleasy build prodeasy zip --deps --nodejs
这里仅仅把代码构建文件一起压缩成 zip, 这样线上在启动时需要运行 npm install –production 安装依赖。
easy clean alleasy build prodeasy zip
npm start
如果不是用 egg-scripts start
启动应用, 请配置 EGG_SERVER_ENV 环境变量 EGG_SERVER_ENV=prod NODE_ENV=production
配置环境变量.
目前 egg-view-vue-ssr 支持 服务端渲染模式 和 前端渲染模式 两种渲染模式
这里服务端渲染指的是编写的 Vue 组件在 Node 服务端直接编译成完整的HTML, 然后直接输出给浏览器。MVVM 服务端渲染相比前端渲染,支持SEO,更快的首屏渲染,相比传统的模板引擎,更好的组件化,前后端模板共用。 同时 MVVM 数据驱动方式有着更快的开发效率。总体来说,MVVM 框架的服务端渲染技术比较适合有一定交互性,且对SEO,首屏速度有要求的业务应用。当然, 如果想用于不属于该类型的项目(比如各种后台管理系统)也是可以的, 就当纯粹的玩一玩 Vue SSR 开发。
egg-view-vue-ssr
的 render
或 renderToHtml
方法实现服务端渲染// controller/home.jsmodule.exports = app => { return class HomeController extends app.Controller { async index() { const { ctx } = this; await ctx.render('home/home.js', Model.getPage(1, 10)); } async index2() { const { ctx } = this; const html = await ctx.renderToHtml('home/home.js', Model.getPage(1, 10)); // 这里可以处理对渲染后的 HTML 进行处理 ctx.body = html; } };};
home/home.js
是由 Webpack(target:node
) 把 Vue 变成 Node 服务端运行的运行文件, 默认在 ${app_root}/app/view
目录下。
Model.getPage(1, 10)
表示在 Node 服务端获取到的业务数据,传给 Vue 组件在 Node 端进行模板编译为 HTML
Node 编译 HTML之后会根据 config/manifest.json
文件把 css, js 资源依赖注入到 HTML
当服务队渲染失败时, egg-view-vue-ssr
默认开启进行客户端渲染模式。当线上流量过大时, 可以根据一定策略一部分用户服务端渲染, 一部分用户前端渲染, 减少服务端压力。
本地开发默认禁用缓存, 线上运行模式默认开启缓存。
如果是 SPA SSR 应用, 一般是在 Vue 里面提供组件的 fetch 方法由 Node 进行 fetch 数据调用, 然后把数据放入 store, 而不是在 Node 端进行获取, 具体见egg-vue-webpack-boilerplate 功能实现。 如果是单页面服务端渲染,一定注意 store 的创建时机,否则 store 全局共享,内存泄漏,请见下面 38 行代码。
import Vue from 'vue';import { sync } from 'vuex-router-sync';export default class App { constructor(config) { this.config = config; } bootstrap() { if (EASY_ENV_IS_NODE) { return this.server(); } return this.client(); } create(initState) { const { index, options, createStore, createRouter } = this.config; const store = createStore(initState); const router = createRouter(); sync(store, router); return { ...index, ...options, router, store }; } client() { Vue.prototype.$http = require('axios'); const options = this.create(window.__INITIAL_STATE__); const app = new Vue(options); const root = document.getElementById('app'); const hydrate = root.childNodes.length > 0; app.$mount(root, hydrate); return app; } server() { return context => { // store 和 router 一定要在这里面创建,否则 store 全局共享,内存泄漏 const options = this.create(); const { store, router } = options; router.push(context.state.url); return new Promise((resolve, reject) => { router.onReady(() => { const matchedComponents = router.getMatchedComponents(); if (!matchedComponents) { return reject({ code: '404' }); } return Promise.all( matchedComponents.map(component => { if (component.preFetch) { return component.preFetch(store); } return null; }) ).then(() => { context.state = { ...store.state, ...context.state }; return resolve(new Vue(options)); }); }); }); }; }}
easywebpack
默认生成的 service-worker.js
是在 ${app_root}/public/service-worker.js
这里. 这样 service-worker.js
访问路径是 http://127.0.0.1:7001/public/service-worker.js。
将 service worker 文件注册为 /public/service-worker.js
,那么,service worker 只能收到 /public/ 路径下的 fetch
事件(例如: /public/page1/, /public/page2/), 但我们的页面访问是没有 /public/ 这一层路径的。正常情况下, service-worker.js
文件被放在这个域的根目录下,和网站同源。这个 service worker 将会收到这个域下的所有 fetch 事件。
这个问题可以通过 egg-serviceworker 解决。通过 egg-serviceworker
插件, 我们可以这样访问 http://127.0.0.1:7001/service-worker.js
egg-serviceworker
// ${app_root}/config/plugin.jsexports.serviceworker = { enable: true, package: 'egg-serviceworker'};
service worker
const serviceWorkerRegister = require('service-worker-register');// The service-worker.js name will get really url address by sw-mapping.json fileserviceWorkerRegister.default.register('service-worker.js');
配置本地开发启动自动打开 localhost 域名地址
// ${app_root}/config/config.local.jsexports.webpack = { browser: 'http://localhost:7001'};
注意:因开发环境构建的文件在内存中,sw-precache 获取不到文件列表,目前开发环境是不会注册的。可以通过 发布模式 在本地查看
service worker
注册情况。
npm run build:testnpm run start:test
或
npm run buildnpm run start
]]>这里仅提供代码基本实现,请根据项目实际情况进行修改。
${app_root}/app/web/framework/app.js
import Vue from 'vue';import { sync } from 'vuex-router-sync';import './vue/filter';import './vue/directive';export default class App { constructor(config) { this.config = config; } bootstrap() { if (EASY_ENV_IS_NODE) { return this.server(); } return this.client(); } create(initState) { const { index, options, createStore, createRouter } = this.config; const store = createStore(initState); const router = createRouter(); sync(store, router); return { ...index, ...options, router, store }; } client() { Vue.prototype.$http = require('axios'); const options = this.create(window.__INITIAL_STATE__); const app = new Vue(options); app.$mount('#app'); return app; } server() { return context => { const options = this.create(); const { store, router } = options; router.push(context.state.url); return new Promise((resolve, reject) => { router.onReady(() => { const matchedComponents = router.getMatchedComponents(); if (!matchedComponents) { return reject({ code: '404' }); } return Promise.all( matchedComponents.map(component => { if (component.methods && component.methods.fetchApi) { return component.methods.fetchApi(store); } return null; }) ).then(() => { context.state = { ...store.state, ...context.state }; return resolve(new Vue(options)); }); }); }); }; }
${app_root}/app/web/page/home/home.js
webpack entry 入口文件这种方式适合需要自定义实现入口代码的比较少页面入口项目,如果项目有多个单独页面,就需要编写下面类似的重复代码,但通过公共代码抽离,问题也不是太大,能满足所有自定义要求,这个完全交给项目自己去实现。
'use strict';import App from 'framework/app.js';import index from './index.vue';import createStore from './store';import createRouter from './router';const options = { base: '/' };export default new App({ index, options, createStore, createRouter,}).bootstrap();
详细实现请见:https://github.com/easy-team/egg-vue-webpack-boilerplate/tree/feature/green/spa
easywebpack 提供了通过 配置 entry.loader 实现入口代码模板化,并且代码模板完全有项目自己实现. 项目只需要实现对应的 loader 即可。这里仅提供代码基本实现,请根据实际项目情况进行修改。
${app_root}/app/web/framework/vue/entry/server-loader.js
'use strict';module.exports = function(source) { this.cacheable(); return ` import Vue from 'vue'; import vm from '${this.resourcePath.replace(/\\/g, '\\\\')}'; export default function(context) { const store = typeof vm.store === 'function' ? vm.store(context.state) : vm.store; const router = typeof vm.router === 'function' ? vm.router() : vm.router; if (store && router) { const sync = require('vuex-router-sync').sync; sync(store, router); router.push(context.state.url); return new Promise((resolve, reject) => { router.onReady(() => { const matchedComponents = router.getMatchedComponents(); if (!matchedComponents) { return reject({ code: '404' }); } return Promise.all( matchedComponents.map(component => { if (component.methods && component.methods.fetchApi) { return component.methods.fetchApi(store); } return null; }) ).then(() => { context.state = { ...store.state, ...context.state }; const instanceOptions = { ...vm, store, router, }; return resolve(new Vue(instanceOptions)); }); }); }); } const VueApp = Vue.extend(vm); const instanceOptions = { ...vm, data: context.state }; return new VueApp(instanceOptions); }; `;};
${app_root}/app/web/framework/vue/entry/client-loader.js
'use strict';module.exports = function(source) { return ` import Vue from 'vue'; import vm from '${this.resourcePath.replace(/\\/g, '\\\\')}'; const initState = window.__INITIAL_STATE__ || {}; const context = { state: initState }; const store = typeof vm.store === 'function' ? vm.store(initState) : vm.store; const router = typeof vm.router === 'function' ? vm.router() : vm.router; const data = typeof vm.data === 'function' ? vm.data() : {}; const options = store && router ? { ...vm, store, router } : { ...vm, ...{ data() { return { ...initState, ...data}; } } }; const app = new Vue(options); app.$mount('#app'); `;};
'use strict';module.exports = { egg: true, framework: 'vue', entry: { include: ['app/web/page'], exclude: ['app/web/page/[a-z]+/component', 'app/web/page/test'], loader: { client: 'app/web/framework/vue/entry/client-loader.js', server: 'app/web/framework/vue/entry/server-loader.js', } } }
具体例子可以参考:https://github.com/easy-team/egg-vue-webpack-boilerplate/tree/feature/green/multi
easywebpack@4.8.0 开始支持默认 entry node-glob 配置模式。node-glob 模式会自动遍历
app/web/page
目录的所有 .vue 文件作为 entry 入口,排除component|components|view|views
目录下的文件。 如果 entry 是已 .vue 文件,则使用 vue-entry-loader 作为模板入口。注意:只有 entry 文件是 .vue 文件(非.js)时,才会自动使用 **vue-entry-loader 模板。**
// webpack.config.jsmodule.exports = { // 注意 只有 entry 文件是 .vue 文件(非.js)时,才会自动使用 vue-entry-loader模板 entry: 'app/web/page/**!(component|components|view|views)/*.vue'}
module.exports = { entry: { app: 'app/web/page/app/index.js', // js 文件需要自己实现初始化逻辑,这个时候可以结合方案一 list: 'app/web/page/list/index.vue' // 自动使用 vue-entry-loader模板 }};